Merge remote-tracking branch 'upstream/develop' into fix/end-of-line-emoji
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
fd022310b3
1196 changed files with 72417 additions and 34899 deletions
|
@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {KeyboardEvent} from "react";
|
||||
import { KeyboardEvent } from "react";
|
||||
|
||||
import {Part, CommandPartCreator, PartCreator} from "./parts";
|
||||
import { Part, CommandPartCreator, PartCreator } from "./parts";
|
||||
import DocumentPosition from "./position";
|
||||
import {ICompletion} from "../autocomplete/Autocompleter";
|
||||
import { ICompletion } from "../autocomplete/Autocompleter";
|
||||
import Autocomplete from "../components/views/rooms/Autocomplete";
|
||||
|
||||
export interface ICallback {
|
||||
|
@ -32,7 +32,6 @@ export type GetAutocompleterComponent = () => Autocomplete;
|
|||
export type UpdateQuery = (test: string) => Promise<void>;
|
||||
|
||||
export default class AutocompleteWrapperModel {
|
||||
private queryPart: Part;
|
||||
private partIndex: number;
|
||||
|
||||
constructor(
|
||||
|
@ -43,81 +42,61 @@ export default class AutocompleteWrapperModel {
|
|||
) {
|
||||
}
|
||||
|
||||
public onEscape(e: KeyboardEvent) {
|
||||
public onEscape(e: KeyboardEvent): void {
|
||||
this.getAutocompleterComponent().onEscape(e);
|
||||
this.updateCallback({
|
||||
replaceParts: [this.partCreator.plain(this.queryPart.text)],
|
||||
close: true,
|
||||
});
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.updateCallback({close: true});
|
||||
public close(): void {
|
||||
this.updateCallback({ close: true });
|
||||
}
|
||||
|
||||
public hasSelection() {
|
||||
public hasSelection(): boolean {
|
||||
return this.getAutocompleterComponent().hasSelection();
|
||||
}
|
||||
|
||||
public hasCompletions() {
|
||||
public hasCompletions(): boolean {
|
||||
const ac = this.getAutocompleterComponent();
|
||||
return ac && ac.countCompletions() > 0;
|
||||
}
|
||||
|
||||
public onEnter() {
|
||||
this.updateCallback({close: true});
|
||||
public async confirmCompletion(): Promise<void> {
|
||||
await this.getAutocompleterComponent().onConfirmCompletion();
|
||||
this.updateCallback({ close: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is no current autocompletion, start one and move to the first selection.
|
||||
*/
|
||||
public async startSelection() {
|
||||
public async startSelection(): Promise<void> {
|
||||
const acComponent = this.getAutocompleterComponent();
|
||||
if (acComponent.countCompletions() === 0) {
|
||||
// Force completions to show for the text currently entered
|
||||
await acComponent.forceComplete();
|
||||
// Select the first item by moving "down"
|
||||
await acComponent.moveSelection(+1);
|
||||
}
|
||||
}
|
||||
|
||||
public selectPreviousSelection() {
|
||||
public selectPreviousSelection(): void {
|
||||
this.getAutocompleterComponent().moveSelection(-1);
|
||||
}
|
||||
|
||||
public selectNextSelection() {
|
||||
public selectNextSelection(): void {
|
||||
this.getAutocompleterComponent().moveSelection(+1);
|
||||
}
|
||||
|
||||
public 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;
|
||||
public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> {
|
||||
this.partIndex = pos.index;
|
||||
return this.updateQuery(part.text);
|
||||
}
|
||||
|
||||
public onComponentSelectionChange(completion: ICompletion) {
|
||||
if (!completion) {
|
||||
this.updateCallback({
|
||||
replaceParts: [this.queryPart],
|
||||
});
|
||||
} else {
|
||||
this.updateCallback({
|
||||
replaceParts: this.partForCompletion(completion),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onComponentConfirm(completion: ICompletion) {
|
||||
public onComponentConfirm(completion: ICompletion): void {
|
||||
this.updateCallback({
|
||||
replaceParts: this.partForCompletion(completion),
|
||||
close: true,
|
||||
});
|
||||
}
|
||||
|
||||
private partForCompletion(completion: ICompletion) {
|
||||
const {completionId} = completion;
|
||||
private partForCompletion(completion: ICompletion): Part[] {
|
||||
const { completionId } = completion;
|
||||
const text = completion.completion;
|
||||
switch (completion.type) {
|
||||
case "room":
|
||||
|
@ -125,10 +104,8 @@ export default class AutocompleteWrapperModel {
|
|||
case "at-room":
|
||||
return [this.partCreator.atRoomPill(completionId), this.partCreator.plain(completion.suffix)];
|
||||
case "user":
|
||||
// not using suffix here, because we also need to calculate
|
||||
// the suffix when clicking a display name to insert a mention,
|
||||
// which happens in createMentionParts
|
||||
return this.partCreator.createMentionParts(this.partIndex, text, completionId);
|
||||
// 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);
|
||||
case "command":
|
||||
// command needs special handling for auto complete, but also renders as plain texts
|
||||
return [(this.partCreator as CommandPartCreator).command(text)];
|
||||
|
|
|
@ -15,11 +15,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {needsCaretNodeBefore, needsCaretNodeAfter} from "./render";
|
||||
import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render";
|
||||
import Range from "./range";
|
||||
import EditorModel from "./model";
|
||||
import DocumentPosition, {IPosition} from "./position";
|
||||
import {Part} from "./parts";
|
||||
import DocumentPosition, { IPosition } from "./position";
|
||||
import { Part, Type } from "./parts";
|
||||
|
||||
export type Caret = Range | DocumentPosition;
|
||||
|
||||
|
@ -44,7 +44,7 @@ function setDocumentRangeSelection(editor: HTMLDivElement, model: EditorModel, r
|
|||
|
||||
export function setCaretPosition(editor: HTMLDivElement, model: EditorModel, caretPosition: IPosition) {
|
||||
const range = document.createRange();
|
||||
const {node, offset} = getNodeAndOffsetForPosition(editor, model, caretPosition);
|
||||
const { node, offset } = getNodeAndOffsetForPosition(editor, model, caretPosition);
|
||||
range.setStart(node, offset);
|
||||
range.collapse(true);
|
||||
|
||||
|
@ -68,7 +68,7 @@ export function setCaretPosition(editor: HTMLDivElement, model: EditorModel, car
|
|||
}
|
||||
|
||||
function getNodeAndOffsetForPosition(editor: HTMLDivElement, model: EditorModel, position: IPosition) {
|
||||
const {offset, lineIndex, nodeIndex} = getLineAndNodePosition(model, position);
|
||||
const { offset, lineIndex, nodeIndex } = getLineAndNodePosition(model, position);
|
||||
const lineNode = editor.childNodes[lineIndex];
|
||||
|
||||
let focusNode;
|
||||
|
@ -82,16 +82,16 @@ function getNodeAndOffsetForPosition(editor: HTMLDivElement, model: EditorModel,
|
|||
focusNode = focusNode.firstChild;
|
||||
}
|
||||
}
|
||||
return {node: focusNode, offset};
|
||||
return { node: focusNode, offset };
|
||||
}
|
||||
|
||||
export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosition) {
|
||||
const {parts} = model;
|
||||
const { parts } = model;
|
||||
const partIndex = caretPosition.index;
|
||||
const lineResult = findNodeInLineForPart(parts, partIndex);
|
||||
const {lineIndex} = lineResult;
|
||||
let {nodeIndex} = lineResult;
|
||||
let {offset} = caretPosition;
|
||||
const { lineIndex } = lineResult;
|
||||
let { nodeIndex } = lineResult;
|
||||
let { offset } = caretPosition;
|
||||
// we're at an empty line between a newline part
|
||||
// and another newline part or end/start of parts.
|
||||
// set offset to 0 so it gets set to the <br> inside the line container
|
||||
|
@ -99,9 +99,9 @@ export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosit
|
|||
offset = 0;
|
||||
} else {
|
||||
// move caret out of uneditable part (into caret node, or empty line br) if needed
|
||||
({nodeIndex, offset} = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset));
|
||||
({ nodeIndex, offset } = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset));
|
||||
}
|
||||
return {lineIndex, nodeIndex, offset};
|
||||
return { lineIndex, nodeIndex, offset };
|
||||
}
|
||||
|
||||
function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||
|
@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
|||
// to find newline parts
|
||||
for (let i = 0; i <= partIndex; ++i) {
|
||||
const part = parts[i];
|
||||
if (part.type === "newline") {
|
||||
if (part.type === Type.Newline) {
|
||||
lineIndex += 1;
|
||||
nodeIndex = -1;
|
||||
prevPart = null;
|
||||
|
@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
|||
// and not an adjacent caret node
|
||||
if (i < partIndex) {
|
||||
const nextPart = parts[i + 1];
|
||||
const isLastOfLine = !nextPart || nextPart.type === "newline";
|
||||
const isLastOfLine = !nextPart || nextPart.type === Type.Newline;
|
||||
if (needsCaretNodeAfter(part, isLastOfLine)) {
|
||||
nodeIndex += 1;
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
|||
}
|
||||
}
|
||||
|
||||
return {lineIndex, nodeIndex};
|
||||
return { lineIndex, nodeIndex };
|
||||
}
|
||||
|
||||
function moveOutOfUneditablePart(parts: Part[], partIndex: number, nodeIndex: number, offset: number) {
|
||||
|
@ -159,5 +159,5 @@ function moveOutOfUneditablePart(parts: Part[], partIndex: number, nodeIndex: nu
|
|||
offset = 0;
|
||||
}
|
||||
}
|
||||
return {nodeIndex, offset};
|
||||
return { nodeIndex, offset };
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import { walkDOMDepthFirst } from "./dom";
|
||||
import { checkBlockNode } from "../HtmlUtils";
|
||||
import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks";
|
||||
import { PartCreator } from "./parts";
|
||||
import { PartCreator, Type } from "./parts";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
||||
|
@ -42,7 +42,7 @@ function parseAtRoomMentions(text: string, partCreator: PartCreator) {
|
|||
}
|
||||
|
||||
function parseLink(a: HTMLAnchorElement, partCreator: PartCreator) {
|
||||
const {href} = a;
|
||||
const { href } = a;
|
||||
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
|
||||
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
|
||||
switch (prefix) {
|
||||
|
@ -121,6 +121,12 @@ function parseElement(n: HTMLElement, partCreator: PartCreator, lastNode: HTMLEl
|
|||
return partCreator.plain(`\`${n.textContent}\``);
|
||||
case "DEL":
|
||||
return partCreator.plain(`<del>${n.textContent}</del>`);
|
||||
case "SUB":
|
||||
return partCreator.plain(`<sub>${n.textContent}</sub>`);
|
||||
case "SUP":
|
||||
return partCreator.plain(`<sup>${n.textContent}</sup>`);
|
||||
case "U":
|
||||
return partCreator.plain(`<u>${n.textContent}</u>`);
|
||||
case "LI": {
|
||||
const indent = " ".repeat(state.listDepth - 1);
|
||||
if (n.parentElement.nodeName === "OL") {
|
||||
|
@ -200,7 +206,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) {
|
|||
parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
||||
}
|
||||
for (let i = 0; i < parts.length; i += 1) {
|
||||
if (parts[i].type === "newline") {
|
||||
if (parts[i].type === Type.Newline) {
|
||||
parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX));
|
||||
i += 1;
|
||||
}
|
||||
|
@ -297,7 +303,7 @@ export function parsePlainTextMessage(body: string, partCreator: PartCreator, is
|
|||
}, []);
|
||||
}
|
||||
|
||||
export function parseEvent(event: MatrixEvent, partCreator: PartCreator, {isQuotedMessage = false} = {}) {
|
||||
export function parseEvent(event: MatrixEvent, partCreator: PartCreator, { isQuotedMessage = false } = {}) {
|
||||
const content = event.getContent();
|
||||
let parts;
|
||||
if (content.format === "org.matrix.custom.html") {
|
||||
|
|
|
@ -21,7 +21,7 @@ export interface IDiff {
|
|||
at?: number;
|
||||
}
|
||||
|
||||
function firstDiff(a: string, b: string) {
|
||||
function firstDiff(a: string, b: string): number {
|
||||
const compareLen = Math.min(a.length, b.length);
|
||||
for (let i = 0; i < compareLen; ++i) {
|
||||
if (a[i] !== b[i]) {
|
||||
|
@ -35,9 +35,9 @@ function diffStringsAtEnd(oldStr: string, newStr: string): IDiff {
|
|||
const len = Math.min(oldStr.length, newStr.length);
|
||||
const startInCommon = oldStr.substr(0, len) === newStr.substr(0, len);
|
||||
if (startInCommon && oldStr.length > newStr.length) {
|
||||
return {removed: oldStr.substr(len), at: len};
|
||||
return { removed: oldStr.substr(len), at: len };
|
||||
} else if (startInCommon && oldStr.length < newStr.length) {
|
||||
return {added: newStr.substr(len), at: len};
|
||||
return { added: newStr.substr(len), at: len };
|
||||
} else {
|
||||
const commonStartLen = firstDiff(oldStr, newStr);
|
||||
return {
|
||||
|
@ -55,7 +55,7 @@ export function diffDeletion(oldStr: string, newStr: string): IDiff {
|
|||
}
|
||||
const firstDiffIdx = firstDiff(oldStr, newStr);
|
||||
const amount = oldStr.length - newStr.length;
|
||||
return {at: firstDiffIdx, removed: oldStr.substr(firstDiffIdx, amount)};
|
||||
return { at: firstDiffIdx, removed: oldStr.substr(firstDiffIdx, amount) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {CARET_NODE_CHAR, isCaretNode} from "./render";
|
||||
import { CARET_NODE_CHAR, isCaretNode } from "./render";
|
||||
import DocumentOffset from "./offset";
|
||||
|
||||
type Predicate = (node: Node) => boolean;
|
||||
|
@ -44,8 +44,8 @@ export function walkDOMDepthFirst(rootNode: Node, enterNodeCallback: Predicate,
|
|||
}
|
||||
|
||||
export function getCaretOffsetAndText(editor: HTMLDivElement, sel: Selection) {
|
||||
const {offset, text} = getSelectionOffsetAndText(editor, sel.focusNode, sel.focusOffset);
|
||||
return {caret: offset, text};
|
||||
const { offset, text } = getSelectionOffsetAndText(editor, sel.focusNode, sel.focusOffset);
|
||||
return { caret: offset, text };
|
||||
}
|
||||
|
||||
function tryReduceSelectionToTextNode(selectionNode: Node, selectionOffset: number) {
|
||||
|
@ -86,10 +86,10 @@ function tryReduceSelectionToTextNode(selectionNode: Node, selectionOffset: numb
|
|||
}
|
||||
|
||||
function getSelectionOffsetAndText(editor: HTMLDivElement, selectionNode: Node, selectionOffset: number) {
|
||||
const {node, characterOffset} = tryReduceSelectionToTextNode(selectionNode, selectionOffset);
|
||||
const {text, offsetToNode} = getTextAndOffsetToNode(editor, node);
|
||||
const { node, characterOffset } = tryReduceSelectionToTextNode(selectionNode, selectionOffset);
|
||||
const { text, offsetToNode } = getTextAndOffsetToNode(editor, node);
|
||||
const offset = getCaret(node, offsetToNode, characterOffset);
|
||||
return {offset, text};
|
||||
return { offset, text };
|
||||
}
|
||||
|
||||
// gets the caret position details, ignoring and adjusting to
|
||||
|
@ -163,7 +163,7 @@ function getTextAndOffsetToNode(editor: HTMLDivElement, selectionNode: Node) {
|
|||
|
||||
walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback);
|
||||
|
||||
return {text, offsetToNode};
|
||||
return { text, offsetToNode };
|
||||
}
|
||||
|
||||
// get text value of text node, ignoring ZWS if it's a caret node
|
||||
|
|
|
@ -15,9 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EditorModel from "./model";
|
||||
import {IDiff} from "./diff";
|
||||
import {SerializedPart} from "./parts";
|
||||
import {Caret} from "./caret";
|
||||
import { IDiff } from "./diff";
|
||||
import { SerializedPart } from "./parts";
|
||||
import { Caret } from "./caret";
|
||||
|
||||
interface IHistory {
|
||||
parts: SerializedPart[];
|
||||
|
@ -36,7 +36,7 @@ export default class HistoryManager {
|
|||
private addedSinceLastPush = false;
|
||||
private removedSinceLastPush = false;
|
||||
|
||||
clear() {
|
||||
public clear(): void {
|
||||
this.stack = [];
|
||||
this.newlyTypedCharCount = 0;
|
||||
this.currentIndex = -1;
|
||||
|
@ -92,7 +92,7 @@ export default class HistoryManager {
|
|||
this.stack.pop();
|
||||
}
|
||||
const parts = model.serializeParts();
|
||||
this.stack.push({parts, caret});
|
||||
this.stack.push({ parts, caret });
|
||||
this.currentIndex = this.stack.length - 1;
|
||||
this.lastCaret = null;
|
||||
this.changedSinceLastPush = false;
|
||||
|
@ -103,7 +103,7 @@ export default class HistoryManager {
|
|||
}
|
||||
|
||||
// needs to persist parts and caret position
|
||||
tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) {
|
||||
public tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff): boolean {
|
||||
// ignore state restoration echos.
|
||||
// these respect the inputType values of the input event,
|
||||
// but are actually passed in from MessageEditor calling model.reset()
|
||||
|
@ -121,22 +121,22 @@ export default class HistoryManager {
|
|||
return shouldPush;
|
||||
}
|
||||
|
||||
ensureLastChangesPushed(model: EditorModel) {
|
||||
public ensureLastChangesPushed(model: EditorModel): void {
|
||||
if (this.changedSinceLastPush) {
|
||||
this.pushState(model, this.lastCaret);
|
||||
}
|
||||
}
|
||||
|
||||
canUndo() {
|
||||
public canUndo(): boolean {
|
||||
return this.currentIndex >= 1 || this.changedSinceLastPush;
|
||||
}
|
||||
|
||||
canRedo() {
|
||||
public canRedo(): boolean {
|
||||
return this.currentIndex < (this.stack.length - 1);
|
||||
}
|
||||
|
||||
// returns state that should be applied to model
|
||||
undo(model: EditorModel) {
|
||||
public undo(model: EditorModel): IHistory {
|
||||
if (this.canUndo()) {
|
||||
this.ensureLastChangesPushed(model);
|
||||
this.currentIndex -= 1;
|
||||
|
@ -145,7 +145,7 @@ export default class HistoryManager {
|
|||
}
|
||||
|
||||
// returns state that should be applied to model
|
||||
redo() {
|
||||
public redo(): IHistory {
|
||||
if (this.canRedo()) {
|
||||
this.changedSinceLastPush = false;
|
||||
this.currentIndex += 1;
|
||||
|
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {diffAtCaret, diffDeletion, IDiff} from "./diff";
|
||||
import DocumentPosition, {IPosition} from "./position";
|
||||
import { diffAtCaret, diffDeletion, IDiff } from "./diff";
|
||||
import DocumentPosition, { IPosition } from "./position";
|
||||
import Range from "./range";
|
||||
import {SerializedPart, Part, PartCreator} from "./parts";
|
||||
import AutocompleteWrapperModel, {ICallback} from "./autocomplete";
|
||||
import { SerializedPart, Part, PartCreator } from "./parts";
|
||||
import AutocompleteWrapperModel, { ICallback } from "./autocomplete";
|
||||
import DocumentOffset from "./offset";
|
||||
import {Caret} from "./caret";
|
||||
import { Caret } from "./caret";
|
||||
|
||||
/**
|
||||
* @callback ModelCallback
|
||||
|
@ -70,7 +70,7 @@ export default class EditorModel {
|
|||
* on the model that can span multiple parts. Also see `startRange()`.
|
||||
* @param {TransformCallback} transformCallback
|
||||
*/
|
||||
setTransformCallback(transformCallback: TransformCallback) {
|
||||
public setTransformCallback(transformCallback: TransformCallback): void {
|
||||
this.transformCallback = transformCallback;
|
||||
}
|
||||
|
||||
|
@ -78,23 +78,23 @@ export default class EditorModel {
|
|||
* Set a callback for rerendering the model after it has been updated.
|
||||
* @param {ModelCallback} updateCallback
|
||||
*/
|
||||
setUpdateCallback(updateCallback: UpdateCallback) {
|
||||
public setUpdateCallback(updateCallback: UpdateCallback): void {
|
||||
this.updateCallback = updateCallback;
|
||||
}
|
||||
|
||||
get partCreator() {
|
||||
public get partCreator(): PartCreator {
|
||||
return this._partCreator;
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
public get isEmpty(): boolean {
|
||||
return this._parts.reduce((len, part) => len + part.text.length, 0) === 0;
|
||||
}
|
||||
|
||||
clone() {
|
||||
public clone(): EditorModel {
|
||||
return new EditorModel(this._parts, this._partCreator, this.updateCallback);
|
||||
}
|
||||
|
||||
private insertPart(index: number, part: Part) {
|
||||
private insertPart(index: number, part: Part): void {
|
||||
this._parts.splice(index, 0, part);
|
||||
if (this.activePartIdx >= index) {
|
||||
++this.activePartIdx;
|
||||
|
@ -104,7 +104,7 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
private removePart(index: number) {
|
||||
private removePart(index: number): void {
|
||||
this._parts.splice(index, 1);
|
||||
if (index === this.activePartIdx) {
|
||||
this.activePartIdx = null;
|
||||
|
@ -118,22 +118,22 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
private replacePart(index: number, part: Part) {
|
||||
private replacePart(index: number, part: Part): void {
|
||||
this._parts.splice(index, 1, part);
|
||||
}
|
||||
|
||||
get parts() {
|
||||
public get parts(): Part[] {
|
||||
return this._parts;
|
||||
}
|
||||
|
||||
get autoComplete() {
|
||||
public get autoComplete(): AutocompleteWrapperModel {
|
||||
if (this.activePartIdx === this.autoCompletePartIdx) {
|
||||
return this._autoComplete;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getPositionAtEnd() {
|
||||
public getPositionAtEnd(): DocumentPosition {
|
||||
if (this._parts.length) {
|
||||
const index = this._parts.length - 1;
|
||||
const part = this._parts[index];
|
||||
|
@ -144,11 +144,11 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
serializeParts() {
|
||||
public serializeParts(): SerializedPart[] {
|
||||
return this._parts.map(p => p.serialize());
|
||||
}
|
||||
|
||||
private diff(newValue: string, inputType: string, caret: DocumentOffset) {
|
||||
private diff(newValue: string, inputType: string, caret: DocumentOffset): IDiff {
|
||||
const previousValue = this.parts.reduce((text, p) => text + p.text, "");
|
||||
// can't use caret position with drag and drop
|
||||
if (inputType === "deleteByDrag") {
|
||||
|
@ -158,7 +158,7 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
|
||||
reset(serializedParts: SerializedPart[], caret: Caret, inputType: string) {
|
||||
public reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string): void {
|
||||
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: Part[], position: IPosition) {
|
||||
public insert(parts: Part[], position: IPosition): number {
|
||||
const insertIndex = this.splitAt(position);
|
||||
let newTextLength = 0;
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
|
@ -191,7 +191,7 @@ export default class EditorModel {
|
|||
return newTextLength;
|
||||
}
|
||||
|
||||
update(newValue: string, inputType: string, caret: DocumentOffset) {
|
||||
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);
|
||||
let removedOffsetDecrease = 0;
|
||||
|
@ -220,8 +220,8 @@ export default class EditorModel {
|
|||
return Number.isFinite(result) ? result as number : 0;
|
||||
}
|
||||
|
||||
private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean) {
|
||||
const {index} = pos;
|
||||
private setActivePart(pos: DocumentPosition, canOpenAutoComplete: boolean): Promise<void> {
|
||||
const { index } = pos;
|
||||
const part = this._parts[index];
|
||||
if (part) {
|
||||
if (index !== this.activePartIdx) {
|
||||
|
@ -237,7 +237,7 @@ export default class EditorModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
// not _autoComplete, only there if active part is autocomplete part
|
||||
// not autoComplete, only there if active part is autocomplete part
|
||||
if (this.autoComplete) {
|
||||
return this.autoComplete.onPartUpdate(part, pos);
|
||||
}
|
||||
|
@ -250,7 +250,7 @@ export default class EditorModel {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private onAutoComplete = ({replaceParts, close}: ICallback) => {
|
||||
private onAutoComplete = ({ replaceParts, close }: ICallback): void => {
|
||||
let pos;
|
||||
if (replaceParts) {
|
||||
this._parts.splice(this.autoCompletePartIdx, this.autoCompletePartCount, ...replaceParts);
|
||||
|
@ -270,7 +270,7 @@ export default class EditorModel {
|
|||
this.updateCallback(pos);
|
||||
};
|
||||
|
||||
private mergeAdjacentParts() {
|
||||
private mergeAdjacentParts(): void {
|
||||
let prevPart;
|
||||
for (let i = 0; i < this._parts.length; ++i) {
|
||||
let part = this._parts[i];
|
||||
|
@ -294,8 +294,8 @@ export default class EditorModel {
|
|||
* @return {Number} how many characters before pos were also removed,
|
||||
* usually because of non-editable parts that can only be removed in their entirety.
|
||||
*/
|
||||
removeText(pos: IPosition, len: number) {
|
||||
let {index, offset} = pos;
|
||||
public removeText(pos: IPosition, len: number): number {
|
||||
let { index, offset } = pos;
|
||||
let removedOffsetDecrease = 0;
|
||||
while (len > 0) {
|
||||
// part might be undefined here
|
||||
|
@ -329,7 +329,7 @@ export default class EditorModel {
|
|||
}
|
||||
|
||||
// return part index where insertion will insert between at offset
|
||||
private splitAt(pos: IPosition) {
|
||||
private splitAt(pos: IPosition): number {
|
||||
if (pos.index === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -356,9 +356,9 @@ export default class EditorModel {
|
|||
* @return {Number} how far from position (in characters) the insertion ended.
|
||||
* This can be more than the length of `str` when crossing non-editable parts, which are skipped.
|
||||
*/
|
||||
private addText(pos: IPosition, str: string, inputType: string) {
|
||||
let {index} = pos;
|
||||
const {offset} = pos;
|
||||
private addText(pos: IPosition, str: string, inputType: string): number {
|
||||
let { index } = pos;
|
||||
const { offset } = pos;
|
||||
let addLen = str.length;
|
||||
const part = this._parts[index];
|
||||
if (part) {
|
||||
|
@ -390,7 +390,7 @@ export default class EditorModel {
|
|||
return addLen;
|
||||
}
|
||||
|
||||
positionForOffset(totalOffset: number, atPartEnd: boolean) {
|
||||
public positionForOffset(totalOffset: number, atPartEnd = false): DocumentPosition {
|
||||
let currentOffset = 0;
|
||||
const index = this._parts.findIndex(part => {
|
||||
const partLen = part.text.length;
|
||||
|
@ -416,11 +416,11 @@ export default class EditorModel {
|
|||
* @param {DocumentPosition?} positionB the other boundary of the range, optional
|
||||
* @return {Range}
|
||||
*/
|
||||
startRange(positionA: DocumentPosition, positionB = positionA) {
|
||||
public startRange(positionA: DocumentPosition, positionB = positionA): Range {
|
||||
return new Range(this, positionA, positionB);
|
||||
}
|
||||
|
||||
replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]) {
|
||||
public replaceRange(startPosition: DocumentPosition, endPosition: DocumentPosition, parts: Part[]): void {
|
||||
// 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);
|
||||
|
@ -445,9 +445,9 @@ export default class EditorModel {
|
|||
* @param {ManualTransformCallback} callback to run the transformations in
|
||||
* @return {Promise} a promise when auto-complete (if applicable) is done updating
|
||||
*/
|
||||
transform(callback: ManualTransformCallback) {
|
||||
public transform(callback: ManualTransformCallback): Promise<void> {
|
||||
const pos = callback();
|
||||
let acPromise = null;
|
||||
let acPromise: Promise<void> = null;
|
||||
if (!(pos instanceof Range)) {
|
||||
acPromise = this.setActivePart(pos, true);
|
||||
} else {
|
||||
|
|
|
@ -15,16 +15,17 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EditorModel from "./model";
|
||||
import DocumentPosition from "./position";
|
||||
|
||||
export default class DocumentOffset {
|
||||
constructor(public offset: number, public readonly atNodeEnd: boolean) {
|
||||
}
|
||||
|
||||
asPosition(model: EditorModel) {
|
||||
public asPosition(model: EditorModel): DocumentPosition {
|
||||
return model.positionForOffset(this.offset, this.atNodeEnd);
|
||||
}
|
||||
|
||||
add(delta: number, atNodeEnd = false) {
|
||||
public add(delta: number, atNodeEnd = false): DocumentOffset {
|
||||
return new DocumentOffset(this.offset + delta, atNodeEnd);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,14 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Range from "./range";
|
||||
import {Part} from "./parts";
|
||||
import { Part, Type } from "./parts";
|
||||
|
||||
/**
|
||||
* Some common queries and transformations on the editor model
|
||||
*/
|
||||
|
||||
export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
|
||||
const {model} = range;
|
||||
export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]): void {
|
||||
const { model } = range;
|
||||
model.transform(() => {
|
||||
const oldLen = range.length;
|
||||
const addedLen = range.replace(newParts);
|
||||
|
@ -32,8 +32,8 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) {
|
|||
});
|
||||
}
|
||||
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
|
||||
const {model} = range;
|
||||
export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void {
|
||||
const { model } = range;
|
||||
model.transform(() => {
|
||||
const oldLen = range.length;
|
||||
const addedLen = range.replace(newParts);
|
||||
|
@ -43,29 +43,29 @@ export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) {
|
|||
});
|
||||
}
|
||||
|
||||
export function rangeStartsAtBeginningOfLine(range: Range) {
|
||||
const {model} = range;
|
||||
export function rangeStartsAtBeginningOfLine(range: Range): boolean {
|
||||
const { model } = range;
|
||||
const startsWithPartial = range.start.offset !== 0;
|
||||
const isFirstPart = range.start.index === 0;
|
||||
const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline";
|
||||
const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === Type.Newline;
|
||||
return !startsWithPartial && (isFirstPart || previousIsNewline);
|
||||
}
|
||||
|
||||
export function rangeEndsAtEndOfLine(range: Range) {
|
||||
const {model} = range;
|
||||
export function rangeEndsAtEndOfLine(range: Range): boolean {
|
||||
const { model } = range;
|
||||
const lastPart = model.parts[range.end.index];
|
||||
const endsWithPartial = range.end.offset !== lastPart.text.length;
|
||||
const isLastPart = range.end.index === model.parts.length - 1;
|
||||
const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline";
|
||||
const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === Type.Newline;
|
||||
return !endsWithPartial && (isLastPart || nextIsNewline);
|
||||
}
|
||||
|
||||
export function formatRangeAsQuote(range: Range) {
|
||||
const {model, parts} = range;
|
||||
const {partCreator} = model;
|
||||
export function formatRangeAsQuote(range: Range): void {
|
||||
const { model, parts } = range;
|
||||
const { partCreator } = model;
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
const part = parts[i];
|
||||
if (part.type === "newline") {
|
||||
if (part.type === Type.Newline) {
|
||||
parts.splice(i + 1, 0, partCreator.plain("> "));
|
||||
}
|
||||
}
|
||||
|
@ -81,10 +81,10 @@ export function formatRangeAsQuote(range: Range) {
|
|||
replaceRangeAndExpandSelection(range, parts);
|
||||
}
|
||||
|
||||
export function formatRangeAsCode(range: Range) {
|
||||
const {model, parts} = range;
|
||||
const {partCreator} = model;
|
||||
const needsBlock = parts.some(p => p.type === "newline");
|
||||
export function formatRangeAsCode(range: Range): void {
|
||||
const { model, parts } = range;
|
||||
const { partCreator } = model;
|
||||
const needsBlock = parts.some(p => p.type === Type.Newline);
|
||||
if (needsBlock) {
|
||||
parts.unshift(partCreator.plain("```"), partCreator.newline());
|
||||
if (!rangeStartsAtBeginningOfLine(range)) {
|
||||
|
@ -105,11 +105,11 @@ export function formatRangeAsCode(range: Range) {
|
|||
|
||||
// parts helper methods
|
||||
const isBlank = part => !part.text || !/\S/.test(part.text);
|
||||
const isNL = part => part.type === "newline";
|
||||
const isNL = part => part.type === Type.Newline;
|
||||
|
||||
export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) {
|
||||
const {model, parts} = range;
|
||||
const {partCreator} = model;
|
||||
export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix): void {
|
||||
const { model, parts } = range;
|
||||
const { partCreator } = model;
|
||||
|
||||
// compute paragraph [start, end] indexes
|
||||
const paragraphIndexes = [];
|
||||
|
|
|
@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
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 { 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,
|
||||
|
@ -25,6 +25,8 @@ import AutocompleteWrapperModel, {
|
|||
UpdateQuery,
|
||||
} from "./autocomplete";
|
||||
import * as Avatar from "../Avatar";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
|
||||
interface ISerializedPart {
|
||||
type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate;
|
||||
|
@ -34,12 +36,12 @@ interface ISerializedPart {
|
|||
interface ISerializedPillPart {
|
||||
type: Type.AtRoomPill | Type.RoomPill | Type.UserPill;
|
||||
text: string;
|
||||
resourceId: string;
|
||||
resourceId?: string;
|
||||
}
|
||||
|
||||
export type SerializedPart = ISerializedPart | ISerializedPillPart;
|
||||
|
||||
enum Type {
|
||||
export enum Type {
|
||||
Plain = "plain",
|
||||
Newline = "newline",
|
||||
Command = "command",
|
||||
|
@ -57,12 +59,12 @@ interface IBasePart {
|
|||
createAutoComplete(updateCallback: UpdateCallback): void;
|
||||
|
||||
serialize(): SerializedPart;
|
||||
remove(offset: number, len: number): string;
|
||||
remove(offset: number, len: number): string | undefined;
|
||||
split(offset: number): IBasePart;
|
||||
validateAndInsert(offset: number, str: string, inputType: string): boolean;
|
||||
appendUntilRejected(str: string, inputType: string): string;
|
||||
updateDOMNode(node: Node);
|
||||
canUpdateDOMNode(node: Node);
|
||||
appendUntilRejected(str: string, inputType: string): string | undefined;
|
||||
updateDOMNode(node: Node): void;
|
||||
canUpdateDOMNode(node: Node): boolean;
|
||||
toDOMNode(): Node;
|
||||
}
|
||||
|
||||
|
@ -85,19 +87,19 @@ abstract class BasePart {
|
|||
this._text = text;
|
||||
}
|
||||
|
||||
acceptsInsertion(chr: string, offset: number, inputType: string) {
|
||||
protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
acceptsRemoval(position: number, chr: string) {
|
||||
protected acceptsRemoval(position: number, chr: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
merge(part: Part) {
|
||||
public merge(part: Part): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
split(offset: number) {
|
||||
public split(offset: number): IBasePart {
|
||||
const splitText = this.text.substr(offset);
|
||||
this._text = this.text.substr(0, offset);
|
||||
return new PlainPart(splitText);
|
||||
|
@ -105,7 +107,7 @@ abstract class BasePart {
|
|||
|
||||
// removes len chars, or returns the plain text this part should be replaced with
|
||||
// if the part would become invalid if it removed everything.
|
||||
remove(offset: number, len: number) {
|
||||
public remove(offset: number, len: number): string | undefined {
|
||||
// validate
|
||||
const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len);
|
||||
for (let i = offset; i < (len + offset); ++i) {
|
||||
|
@ -118,7 +120,7 @@ abstract class BasePart {
|
|||
}
|
||||
|
||||
// append str, returns the remaining string if a character was rejected.
|
||||
appendUntilRejected(str: string, inputType: string) {
|
||||
public appendUntilRejected(str: string, inputType: string): string | undefined {
|
||||
const offset = this.text.length;
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
const chr = str.charAt(i);
|
||||
|
@ -132,7 +134,7 @@ abstract class BasePart {
|
|||
|
||||
// inserts str at offset if all the characters in str were accepted, otherwise don't do anything
|
||||
// return whether the str was accepted or not.
|
||||
validateAndInsert(offset: number, str: string, inputType: string) {
|
||||
public validateAndInsert(offset: number, str: string, inputType: string): boolean {
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
const chr = str.charAt(i);
|
||||
if (!this.acceptsInsertion(chr, offset + i, inputType)) {
|
||||
|
@ -145,42 +147,42 @@ abstract class BasePart {
|
|||
return true;
|
||||
}
|
||||
|
||||
createAutoComplete(updateCallback: UpdateCallback): void {}
|
||||
public createAutoComplete(updateCallback: UpdateCallback): void {}
|
||||
|
||||
trim(len: number) {
|
||||
protected trim(len: number): string {
|
||||
const remaining = this._text.substr(len);
|
||||
this._text = this._text.substr(0, len);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
get text() {
|
||||
public get text(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
abstract get type(): Type;
|
||||
public abstract get type(): Type;
|
||||
|
||||
get canEdit() {
|
||||
public get canEdit(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
toString() {
|
||||
public toString(): string {
|
||||
return `${this.type}(${this.text})`;
|
||||
}
|
||||
|
||||
serialize(): SerializedPart {
|
||||
public serialize(): SerializedPart {
|
||||
return {
|
||||
type: this.type as ISerializedPart["type"],
|
||||
text: this.text,
|
||||
};
|
||||
}
|
||||
|
||||
abstract updateDOMNode(node: Node);
|
||||
abstract canUpdateDOMNode(node: Node);
|
||||
abstract toDOMNode(): Node;
|
||||
public abstract updateDOMNode(node: Node): void;
|
||||
public abstract canUpdateDOMNode(node: Node): boolean;
|
||||
public abstract toDOMNode(): Node;
|
||||
}
|
||||
|
||||
abstract class PlainBasePart extends BasePart {
|
||||
acceptsInsertion(chr: string, offset: number, inputType: string) {
|
||||
protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean {
|
||||
if (chr === "\n") {
|
||||
return false;
|
||||
}
|
||||
|
@ -203,11 +205,11 @@ abstract class PlainBasePart extends BasePart {
|
|||
return true;
|
||||
}
|
||||
|
||||
toDOMNode() {
|
||||
public toDOMNode(): Node {
|
||||
return document.createTextNode(this.text);
|
||||
}
|
||||
|
||||
merge(part) {
|
||||
public merge(part): boolean {
|
||||
if (part.type === this.type) {
|
||||
this._text = this.text + part.text;
|
||||
return true;
|
||||
|
@ -215,47 +217,49 @@ abstract class PlainBasePart extends BasePart {
|
|||
return false;
|
||||
}
|
||||
|
||||
updateDOMNode(node: Node) {
|
||||
public updateDOMNode(node: Node): void {
|
||||
if (node.textContent !== this.text) {
|
||||
node.textContent = this.text;
|
||||
}
|
||||
}
|
||||
|
||||
canUpdateDOMNode(node: Node) {
|
||||
public canUpdateDOMNode(node: Node): boolean {
|
||||
return node.nodeType === Node.TEXT_NODE;
|
||||
}
|
||||
}
|
||||
|
||||
// exported for unit tests, should otherwise only be used through PartCreator
|
||||
export class PlainPart extends PlainBasePart implements IBasePart {
|
||||
get type(): IBasePart["type"] {
|
||||
public get type(): IBasePart["type"] {
|
||||
return Type.Plain;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PillPart extends BasePart implements IPillPart {
|
||||
export abstract class PillPart extends BasePart implements IPillPart {
|
||||
constructor(public resourceId: string, label) {
|
||||
super(label);
|
||||
}
|
||||
|
||||
acceptsInsertion(chr: string) {
|
||||
protected acceptsInsertion(chr: string): boolean {
|
||||
return chr !== " ";
|
||||
}
|
||||
|
||||
acceptsRemoval(position: number, chr: string) {
|
||||
protected acceptsRemoval(position: number, chr: string): boolean {
|
||||
return position !== 0; //if you remove initial # or @, pill should become plain
|
||||
}
|
||||
|
||||
toDOMNode() {
|
||||
public toDOMNode(): Node {
|
||||
const container = document.createElement("span");
|
||||
container.setAttribute("spellcheck", "false");
|
||||
container.setAttribute("contentEditable", "false");
|
||||
container.onclick = this.onClick;
|
||||
container.className = this.className;
|
||||
container.appendChild(document.createTextNode(this.text));
|
||||
this.setAvatar(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
updateDOMNode(node: HTMLElement) {
|
||||
public updateDOMNode(node: HTMLElement): void {
|
||||
const textNode = node.childNodes[0];
|
||||
if (textNode.textContent !== this.text) {
|
||||
textNode.textContent = this.text;
|
||||
|
@ -263,10 +267,13 @@ abstract class PillPart extends BasePart implements IPillPart {
|
|||
if (node.className !== this.className) {
|
||||
node.className = this.className;
|
||||
}
|
||||
if (node.onclick !== this.onClick) {
|
||||
node.onclick = this.onClick;
|
||||
}
|
||||
this.setAvatar(node);
|
||||
}
|
||||
|
||||
canUpdateDOMNode(node: HTMLElement) {
|
||||
public canUpdateDOMNode(node: HTMLElement): boolean {
|
||||
return node.nodeType === Node.ELEMENT_NODE &&
|
||||
node.nodeName === "SPAN" &&
|
||||
node.childNodes.length === 1 &&
|
||||
|
@ -274,7 +281,7 @@ abstract class PillPart extends BasePart implements IPillPart {
|
|||
}
|
||||
|
||||
// helper method for subclasses
|
||||
_setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) {
|
||||
protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void {
|
||||
const avatarBackground = `url('${avatarUrl}')`;
|
||||
const avatarLetter = `'${initialLetter}'`;
|
||||
// check if the value is changing,
|
||||
|
@ -287,41 +294,51 @@ abstract class PillPart extends BasePart implements IPillPart {
|
|||
}
|
||||
}
|
||||
|
||||
get canEdit() {
|
||||
public serialize(): ISerializedPillPart {
|
||||
return {
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
resourceId: this.resourceId,
|
||||
};
|
||||
}
|
||||
|
||||
public get canEdit(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract get type(): IPillPart["type"];
|
||||
public abstract get type(): IPillPart["type"];
|
||||
|
||||
abstract get className(): string;
|
||||
protected abstract get className(): string;
|
||||
|
||||
abstract setAvatar(node: HTMLElement): void;
|
||||
protected onClick?: () => void;
|
||||
|
||||
protected abstract setAvatar(node: HTMLElement): void;
|
||||
}
|
||||
|
||||
class NewlinePart extends BasePart implements IBasePart {
|
||||
acceptsInsertion(chr: string, offset: number) {
|
||||
protected acceptsInsertion(chr: string, offset: number): boolean {
|
||||
return offset === 0 && chr === "\n";
|
||||
}
|
||||
|
||||
acceptsRemoval(position: number, chr: string) {
|
||||
protected acceptsRemoval(position: number, chr: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
toDOMNode() {
|
||||
public toDOMNode(): Node {
|
||||
return document.createElement("br");
|
||||
}
|
||||
|
||||
merge() {
|
||||
public merge(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateDOMNode() {}
|
||||
public updateDOMNode(): void {}
|
||||
|
||||
canUpdateDOMNode(node: HTMLElement) {
|
||||
public canUpdateDOMNode(node: HTMLElement): boolean {
|
||||
return node.tagName === "BR";
|
||||
}
|
||||
|
||||
get type(): IBasePart["type"] {
|
||||
public get type(): IBasePart["type"] {
|
||||
return Type.Newline;
|
||||
}
|
||||
|
||||
|
@ -329,7 +346,7 @@ class NewlinePart extends BasePart implements IBasePart {
|
|||
// rather than trying to append to it, which is what we want.
|
||||
// As a newline can also be only one character, it makes sense
|
||||
// as it can only be one character long. This caused #9741.
|
||||
get canEdit() {
|
||||
public get canEdit(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -339,25 +356,21 @@ class RoomPillPart extends PillPart {
|
|||
super(resourceId, label);
|
||||
}
|
||||
|
||||
setAvatar(node: HTMLElement) {
|
||||
protected setAvatar(node: HTMLElement): void {
|
||||
let initialLetter = "";
|
||||
let avatarUrl = Avatar.avatarUrlForRoom(
|
||||
this.room,
|
||||
16 * window.devicePixelRatio,
|
||||
16 * window.devicePixelRatio,
|
||||
"crop");
|
||||
let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop");
|
||||
if (!avatarUrl) {
|
||||
initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId);
|
||||
avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId);
|
||||
}
|
||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
this.setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type(): IPillPart["type"] {
|
||||
public get type(): IPillPart["type"] {
|
||||
return Type.RoomPill;
|
||||
}
|
||||
|
||||
get className() {
|
||||
protected get className() {
|
||||
return "mx_RoomPill mx_Pill";
|
||||
}
|
||||
}
|
||||
|
@ -367,9 +380,16 @@ class AtRoomPillPart extends RoomPillPart {
|
|||
super(text, text, room);
|
||||
}
|
||||
|
||||
get type(): IPillPart["type"] {
|
||||
public get type(): IPillPart["type"] {
|
||||
return Type.AtRoomPill;
|
||||
}
|
||||
|
||||
public serialize(): ISerializedPillPart {
|
||||
return {
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UserPillPart extends PillPart {
|
||||
|
@ -377,39 +397,34 @@ class UserPillPart extends PillPart {
|
|||
super(userId, displayName);
|
||||
}
|
||||
|
||||
setAvatar(node: HTMLElement) {
|
||||
public get type(): IPillPart["type"] {
|
||||
return Type.UserPill;
|
||||
}
|
||||
|
||||
protected get className() {
|
||||
return "mx_UserPill mx_Pill";
|
||||
}
|
||||
|
||||
protected setAvatar(node: HTMLElement): void {
|
||||
if (!this.member) {
|
||||
return;
|
||||
}
|
||||
const name = this.member.name || this.member.userId;
|
||||
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
|
||||
const avatarUrl = Avatar.avatarUrlForMember(
|
||||
this.member,
|
||||
16 * window.devicePixelRatio,
|
||||
16 * window.devicePixelRatio,
|
||||
"crop");
|
||||
const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
|
||||
let initialLetter = "";
|
||||
if (avatarUrl === defaultAvatarUrl) {
|
||||
initialLetter = Avatar.getInitialLetter(name);
|
||||
}
|
||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||
this.setAvatarVars(node, avatarUrl, initialLetter);
|
||||
}
|
||||
|
||||
get type(): IPillPart["type"] {
|
||||
return Type.UserPill;
|
||||
}
|
||||
|
||||
get className() {
|
||||
return "mx_UserPill mx_Pill";
|
||||
}
|
||||
|
||||
serialize(): ISerializedPillPart {
|
||||
return {
|
||||
type: this.type,
|
||||
text: this.text,
|
||||
resourceId: this.resourceId,
|
||||
};
|
||||
}
|
||||
protected onClick = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: this.member,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
class PillCandidatePart extends PlainBasePart implements IPillCandidatePart {
|
||||
|
@ -417,11 +432,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart {
|
|||
super(text);
|
||||
}
|
||||
|
||||
createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel {
|
||||
public createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel {
|
||||
return this.autoCompleteCreator.create(updateCallback);
|
||||
}
|
||||
|
||||
acceptsInsertion(chr: string, offset: number, inputType: string) {
|
||||
protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean {
|
||||
if (offset === 0) {
|
||||
return true;
|
||||
} else {
|
||||
|
@ -429,11 +444,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart {
|
|||
}
|
||||
}
|
||||
|
||||
merge() {
|
||||
public merge(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
acceptsRemoval(position: number, chr: string) {
|
||||
protected acceptsRemoval(position: number, chr: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -464,17 +479,21 @@ interface IAutocompleteCreator {
|
|||
export class PartCreator {
|
||||
protected readonly autoCompleteCreator: IAutocompleteCreator;
|
||||
|
||||
constructor(private room: Room, private client: MatrixClient, autoCompleteCreator: AutoCompleteCreator = null) {
|
||||
constructor(
|
||||
private readonly room: Room,
|
||||
private readonly client: MatrixClient,
|
||||
autoCompleteCreator: AutoCompleteCreator = null,
|
||||
) {
|
||||
// pre-create the creator as an object even without callback so it can already be passed
|
||||
// to PillCandidatePart (e.g. while deserializing) and set later on
|
||||
this.autoCompleteCreator = {create: autoCompleteCreator && autoCompleteCreator(this)};
|
||||
this.autoCompleteCreator = { create: autoCompleteCreator?.(this) };
|
||||
}
|
||||
|
||||
setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator) {
|
||||
public setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator): void {
|
||||
this.autoCompleteCreator.create = autoCompleteCreator(this);
|
||||
}
|
||||
|
||||
createPartForInput(input: string, partIndex: number, inputType?: string): Part {
|
||||
public createPartForInput(input: string, partIndex: number, inputType?: string): Part {
|
||||
switch (input[0]) {
|
||||
case "#":
|
||||
case "@":
|
||||
|
@ -488,11 +507,11 @@ export class PartCreator {
|
|||
}
|
||||
}
|
||||
|
||||
createDefaultPart(text: string) {
|
||||
public createDefaultPart(text: string): Part {
|
||||
return this.plain(text);
|
||||
}
|
||||
|
||||
deserializePart(part: SerializedPart): Part {
|
||||
public deserializePart(part: SerializedPart): Part {
|
||||
switch (part.type) {
|
||||
case Type.Plain:
|
||||
return this.plain(part.text);
|
||||
|
@ -503,25 +522,25 @@ export class PartCreator {
|
|||
case Type.PillCandidate:
|
||||
return this.pillCandidate(part.text);
|
||||
case Type.RoomPill:
|
||||
return this.roomPill(part.text);
|
||||
return this.roomPill(part.resourceId);
|
||||
case Type.UserPill:
|
||||
return this.userPill(part.text, part.resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
plain(text: string) {
|
||||
public plain(text: string): PlainPart {
|
||||
return new PlainPart(text);
|
||||
}
|
||||
|
||||
newline() {
|
||||
public newline(): NewlinePart {
|
||||
return new NewlinePart("\n");
|
||||
}
|
||||
|
||||
pillCandidate(text: string) {
|
||||
public pillCandidate(text: string): PillCandidatePart {
|
||||
return new PillCandidatePart(text, this.autoCompleteCreator);
|
||||
}
|
||||
|
||||
roomPill(alias: string, roomId?: string) {
|
||||
public roomPill(alias: string, roomId?: string): RoomPillPart {
|
||||
let room;
|
||||
if (roomId || alias[0] !== "#") {
|
||||
room = this.client.getRoom(roomId || alias);
|
||||
|
@ -534,18 +553,22 @@ export class PartCreator {
|
|||
return new RoomPillPart(alias, room ? room.name : alias, room);
|
||||
}
|
||||
|
||||
atRoomPill(text: string) {
|
||||
public atRoomPill(text: string): AtRoomPillPart {
|
||||
return new AtRoomPillPart(text, this.room);
|
||||
}
|
||||
|
||||
userPill(displayName: string, userId: string) {
|
||||
public userPill(displayName: string, userId: string): UserPillPart {
|
||||
const member = this.room.getMember(userId);
|
||||
return new UserPillPart(userId, displayName, member);
|
||||
}
|
||||
|
||||
createMentionParts(partIndex: number, displayName: string, userId: string) {
|
||||
public createMentionParts(
|
||||
insertTrailingCharacter: boolean,
|
||||
displayName: string,
|
||||
userId: string,
|
||||
): [UserPillPart, PlainPart] {
|
||||
const pill = this.userPill(displayName, userId);
|
||||
const postfix = this.plain(partIndex === 0 ? ": " : " ");
|
||||
const postfix = this.plain(insertTrailingCharacter ? ": " : " ");
|
||||
return [pill, postfix];
|
||||
}
|
||||
}
|
||||
|
@ -553,7 +576,7 @@ export class PartCreator {
|
|||
// part creator that support auto complete for /commands,
|
||||
// used in SendMessageComposer
|
||||
export class CommandPartCreator extends PartCreator {
|
||||
createPartForInput(text: string, partIndex: number) {
|
||||
public createPartForInput(text: string, partIndex: number): Part {
|
||||
// at beginning and starts with /? create
|
||||
if (partIndex === 0 && text[0] === "/") {
|
||||
// text will be inserted by model, so pass empty string
|
||||
|
@ -563,12 +586,12 @@ export class CommandPartCreator extends PartCreator {
|
|||
}
|
||||
}
|
||||
|
||||
command(text: string) {
|
||||
public command(text: string): CommandPart {
|
||||
return new CommandPart(text, this.autoCompleteCreator);
|
||||
}
|
||||
|
||||
deserializePart(part: Part): Part {
|
||||
if (part.type === "command") {
|
||||
public deserializePart(part: SerializedPart): Part {
|
||||
if (part.type === Type.Command) {
|
||||
return this.command(part.text);
|
||||
} else {
|
||||
return super.deserializePart(part);
|
||||
|
@ -577,7 +600,7 @@ export class CommandPartCreator extends PartCreator {
|
|||
}
|
||||
|
||||
class CommandPart extends PillCandidatePart {
|
||||
get type(): IPillCandidatePart["type"] {
|
||||
public get type(): IPillCandidatePart["type"] {
|
||||
return Type.Command;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import DocumentOffset from "./offset";
|
||||
import EditorModel from "./model";
|
||||
import {Part} from "./parts";
|
||||
import { Part } from "./parts";
|
||||
|
||||
export interface IPosition {
|
||||
index: number;
|
||||
|
@ -30,7 +30,7 @@ export default class DocumentPosition implements IPosition {
|
|||
constructor(public readonly index: number, public readonly offset: number) {
|
||||
}
|
||||
|
||||
compare(otherPos: DocumentPosition) {
|
||||
public compare(otherPos: DocumentPosition): number {
|
||||
if (this.index === otherPos.index) {
|
||||
return this.offset - otherPos.offset;
|
||||
} else {
|
||||
|
@ -38,7 +38,7 @@ export default class DocumentPosition implements IPosition {
|
|||
}
|
||||
}
|
||||
|
||||
iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback) {
|
||||
public iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback): void {
|
||||
if (this.index === -1 || other.index === -1) {
|
||||
return;
|
||||
}
|
||||
|
@ -57,13 +57,13 @@ export default class DocumentPosition implements IPosition {
|
|||
}
|
||||
}
|
||||
|
||||
forwardsWhile(model: EditorModel, predicate: Predicate) {
|
||||
public forwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition {
|
||||
if (this.index === -1) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let {index, offset} = this;
|
||||
const {parts} = model;
|
||||
let { index, offset } = this;
|
||||
const { parts } = model;
|
||||
while (index < parts.length) {
|
||||
const part = parts[index];
|
||||
while (offset < part.text.length) {
|
||||
|
@ -82,12 +82,12 @@ export default class DocumentPosition implements IPosition {
|
|||
}
|
||||
}
|
||||
|
||||
backwardsWhile(model: EditorModel, predicate: Predicate) {
|
||||
public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition {
|
||||
if (this.index === -1) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let {index, offset} = this;
|
||||
let { index, offset } = this;
|
||||
const parts = model.parts;
|
||||
while (index >= 0) {
|
||||
const part = parts[index];
|
||||
|
@ -107,7 +107,7 @@ export default class DocumentPosition implements IPosition {
|
|||
}
|
||||
}
|
||||
|
||||
asOffset(model: EditorModel) {
|
||||
public asOffset(model: EditorModel): DocumentOffset {
|
||||
if (this.index === -1) {
|
||||
return new DocumentOffset(0, true);
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ export default class DocumentPosition implements IPosition {
|
|||
return new DocumentOffset(offset, atEnd);
|
||||
}
|
||||
|
||||
isAtEnd(model: EditorModel) {
|
||||
public isAtEnd(model: EditorModel): boolean {
|
||||
if (model.parts.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ export default class DocumentPosition implements IPosition {
|
|||
return this.index === lastPartIdx && this.offset === lastPart.text.length;
|
||||
}
|
||||
|
||||
isAtStart() {
|
||||
public isAtStart(): boolean {
|
||||
return this.index === 0 && this.offset === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import EditorModel from "./model";
|
||||
import DocumentPosition, {Predicate} from "./position";
|
||||
import {Part} from "./parts";
|
||||
import DocumentPosition, { Predicate } from "./position";
|
||||
import { Part } from "./parts";
|
||||
|
||||
const whitespacePredicate: Predicate = (index, offset, part) => {
|
||||
return part.text[offset].trim() === "";
|
||||
|
@ -32,30 +32,30 @@ export default class Range {
|
|||
this._end = bIsLarger ? positionB : positionA;
|
||||
}
|
||||
|
||||
moveStartForwards(delta: number) {
|
||||
public moveStartForwards(delta: number): void {
|
||||
this._start = this._start.forwardsWhile(this.model, () => {
|
||||
delta -= 1;
|
||||
return delta >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
moveEndBackwards(delta: number) {
|
||||
public moveEndBackwards(delta: number): void {
|
||||
this._end = this._end.backwardsWhile(this.model, () => {
|
||||
delta -= 1;
|
||||
return delta >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
trim() {
|
||||
public trim(): void {
|
||||
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
|
||||
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
|
||||
}
|
||||
|
||||
expandBackwardsWhile(predicate: Predicate) {
|
||||
public expandBackwardsWhile(predicate: Predicate): void {
|
||||
this._start = this._start.backwardsWhile(this.model, predicate);
|
||||
}
|
||||
|
||||
get text() {
|
||||
public get text(): string {
|
||||
let text = "";
|
||||
this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => {
|
||||
const t = part.text.substring(startIdx, endIdx);
|
||||
|
@ -70,7 +70,7 @@ export default class Range {
|
|||
* @param {Part[]} parts the parts to replace the range with
|
||||
* @return {Number} the net amount of characters added, can be negative.
|
||||
*/
|
||||
replace(parts: Part[]) {
|
||||
public replace(parts: Part[]): number {
|
||||
const newLength = parts.reduce((sum, part) => sum + part.text.length, 0);
|
||||
let oldLength = 0;
|
||||
this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => {
|
||||
|
@ -84,8 +84,8 @@ export default class Range {
|
|||
* Returns a copy of the (partial) parts within the range.
|
||||
* For partial parts, only the text is adjusted to the part that intersects with the range.
|
||||
*/
|
||||
get parts() {
|
||||
const parts = [];
|
||||
public get parts(): Part[] {
|
||||
const parts: Part[] = [];
|
||||
this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => {
|
||||
const serializedPart = part.serialize();
|
||||
serializedPart.text = part.text.substring(startIdx, endIdx);
|
||||
|
@ -95,7 +95,7 @@ export default class Range {
|
|||
return parts;
|
||||
}
|
||||
|
||||
get length() {
|
||||
public get length(): number {
|
||||
let len = 0;
|
||||
this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => {
|
||||
len += endIdx - startIdx;
|
||||
|
@ -103,11 +103,11 @@ export default class Range {
|
|||
return len;
|
||||
}
|
||||
|
||||
get start() {
|
||||
public get start(): DocumentPosition {
|
||||
return this._start;
|
||||
}
|
||||
|
||||
get end() {
|
||||
public get end(): DocumentPosition {
|
||||
return this._end;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,19 +15,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Part} from "./parts";
|
||||
import { Part, Type } from "./parts";
|
||||
import EditorModel from "./model";
|
||||
|
||||
export function needsCaretNodeBefore(part: Part, prevPart: Part) {
|
||||
const isFirst = !prevPart || prevPart.type === "newline";
|
||||
export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean {
|
||||
const isFirst = !prevPart || prevPart.type === Type.Newline;
|
||||
return !part.canEdit && (isFirst || !prevPart.canEdit);
|
||||
}
|
||||
|
||||
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) {
|
||||
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean {
|
||||
return !part.canEdit && isLastOfLine;
|
||||
}
|
||||
|
||||
function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement) {
|
||||
function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void {
|
||||
const next = node.nextSibling;
|
||||
if (next) {
|
||||
node.parentElement.insertBefore(nodeToInsert, next);
|
||||
|
@ -44,25 +44,25 @@ export const CARET_NODE_CHAR = "\ufeff";
|
|||
// a caret node is a node that allows the caret to be placed
|
||||
// where otherwise it wouldn't be possible
|
||||
// (e.g. next to a pill span without adjacent text node)
|
||||
function createCaretNode() {
|
||||
function createCaretNode(): HTMLElement {
|
||||
const span = document.createElement("span");
|
||||
span.className = "caretNode";
|
||||
span.appendChild(document.createTextNode(CARET_NODE_CHAR));
|
||||
return span;
|
||||
}
|
||||
|
||||
function updateCaretNode(node: HTMLElement) {
|
||||
function updateCaretNode(node: HTMLElement): void {
|
||||
// ensure the caret node contains only a zero-width space
|
||||
if (node.textContent !== CARET_NODE_CHAR) {
|
||||
node.textContent = CARET_NODE_CHAR;
|
||||
}
|
||||
}
|
||||
|
||||
export function isCaretNode(node: HTMLElement) {
|
||||
export function isCaretNode(node: HTMLElement): boolean {
|
||||
return node && node.tagName === "SPAN" && node.className === "caretNode";
|
||||
}
|
||||
|
||||
function removeNextSiblings(node: ChildNode) {
|
||||
function removeNextSiblings(node: ChildNode): void {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ function removeNextSiblings(node: ChildNode) {
|
|||
}
|
||||
}
|
||||
|
||||
function removeChildren(parent: HTMLElement) {
|
||||
function removeChildren(parent: HTMLElement): void {
|
||||
const firstChild = parent.firstChild;
|
||||
if (firstChild) {
|
||||
removeNextSiblings(firstChild);
|
||||
|
@ -82,7 +82,7 @@ function removeChildren(parent: HTMLElement) {
|
|||
}
|
||||
}
|
||||
|
||||
function reconcileLine(lineContainer: ChildNode, parts: Part[]) {
|
||||
function reconcileLine(lineContainer: ChildNode, parts: Part[]): void {
|
||||
let currentNode;
|
||||
let prevPart;
|
||||
const lastPart = parts[parts.length - 1];
|
||||
|
@ -131,13 +131,13 @@ function reconcileLine(lineContainer: ChildNode, parts: Part[]) {
|
|||
removeNextSiblings(currentNode);
|
||||
}
|
||||
|
||||
function reconcileEmptyLine(lineContainer) {
|
||||
function reconcileEmptyLine(lineContainer: HTMLElement): void {
|
||||
// empty div needs to have a BR in it to give it height
|
||||
let foundBR = false;
|
||||
let partNode = lineContainer.firstChild;
|
||||
while (partNode) {
|
||||
const nextNode = partNode.nextSibling;
|
||||
if (!foundBR && partNode.tagName === "BR") {
|
||||
if (!foundBR && (partNode as HTMLElement).tagName === "BR") {
|
||||
foundBR = true;
|
||||
} else {
|
||||
partNode.remove();
|
||||
|
@ -149,9 +149,9 @@ function reconcileEmptyLine(lineContainer) {
|
|||
}
|
||||
}
|
||||
|
||||
export function renderModel(editor: HTMLDivElement, model: EditorModel) {
|
||||
export function renderModel(editor: HTMLDivElement, model: EditorModel): void {
|
||||
const lines = model.parts.reduce((linesArr, part) => {
|
||||
if (part.type === "newline") {
|
||||
if (part.type === Type.Newline) {
|
||||
linesArr.push([]);
|
||||
} else {
|
||||
const lastLine = linesArr[linesArr.length - 1];
|
||||
|
@ -175,7 +175,7 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) {
|
|||
if (parts.length) {
|
||||
reconcileLine(lineContainer, parts);
|
||||
} else {
|
||||
reconcileEmptyLine(lineContainer);
|
||||
reconcileEmptyLine(lineContainer as HTMLElement);
|
||||
}
|
||||
});
|
||||
if (lines.length) {
|
||||
|
|
|
@ -16,36 +16,37 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Markdown from '../Markdown';
|
||||
import {makeGenericPermalink} from "../utils/permalinks/Permalinks";
|
||||
import { makeGenericPermalink } from "../utils/permalinks/Permalinks";
|
||||
import EditorModel from "./model";
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import SettingsStore from '../settings/SettingsStore';
|
||||
import SdkConfig from '../SdkConfig';
|
||||
import cheerio from 'cheerio';
|
||||
import { Type } from './parts';
|
||||
|
||||
export function mdSerialize(model: EditorModel) {
|
||||
export function mdSerialize(model: EditorModel): string {
|
||||
return model.parts.reduce((html, part) => {
|
||||
switch (part.type) {
|
||||
case "newline":
|
||||
case Type.Newline:
|
||||
return html + "\n";
|
||||
case "plain":
|
||||
case "command":
|
||||
case "pill-candidate":
|
||||
case "at-room-pill":
|
||||
case Type.Plain:
|
||||
case Type.Command:
|
||||
case Type.PillCandidate:
|
||||
case Type.AtRoomPill:
|
||||
return html + part.text;
|
||||
case "room-pill":
|
||||
case Type.RoomPill:
|
||||
// Here we use the resourceId for compatibility with non-rich text clients
|
||||
// See https://github.com/vector-im/element-web/issues/16660
|
||||
return html +
|
||||
`[${part.resourceId.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
|
||||
case "user-pill":
|
||||
case Type.UserPill:
|
||||
return html +
|
||||
`[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = {}) {
|
||||
export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string {
|
||||
let md = mdSerialize(model);
|
||||
// copy of raw input to remove unwanted math later
|
||||
const orig = md;
|
||||
|
@ -61,9 +62,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
|
|||
// const inlinePattern = "(?:^|\\s)(?<!\\\\)\\$(?!\\s)(([^$]|\\\\\\$)+?)(?<!\\\\|\\s)\\$";
|
||||
|
||||
// conditions for display math detection $$...$$:
|
||||
// - pattern starts at beginning of line or is not prefixed with backslash or dollar
|
||||
// - pattern starts and ends on a new line
|
||||
// - left delimiter ($$) is not escaped by backslash
|
||||
"display": "(^|[^\\\\$])\\$\\$(([^$]|\\\\\\$)+?)\\$\\$",
|
||||
"display": "(^)\\$\\$(([^$]|\\\\\\$)+?)\\$\\$$",
|
||||
|
||||
// conditions for inline math detection $...$:
|
||||
// - pattern starts at beginning of line, follows whitespace character or punctuation
|
||||
|
@ -78,9 +79,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
|
|||
// detect math with latex delimiters, inline: \(...\), display \[...\]
|
||||
|
||||
// conditions for display math detection \[...\]:
|
||||
// - pattern starts at beginning of line or is not prefixed with backslash
|
||||
// - pattern starts and ends on a new line
|
||||
// - pattern is not empty
|
||||
"display": "(^|[^\\\\])\\\\\\[(?!\\\\\\])(.*?)\\\\\\]",
|
||||
"display": "(^)\\\\\\[(?!\\\\\\])(.*?)\\\\\\]$",
|
||||
|
||||
// conditions for inline math detection \(...\):
|
||||
// - pattern starts at beginning of line or is not prefixed with backslash
|
||||
|
@ -116,14 +117,22 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
|
|||
const parser = new Markdown(md);
|
||||
if (!parser.isPlainText() || forceHTML) {
|
||||
// feed Markdown output to HTML parser
|
||||
const phtml = cheerio.load(parser.toHTML(),
|
||||
{ _useHtmlParser2: true, decodeEntities: false });
|
||||
const phtml = cheerio.load(parser.toHTML(), {
|
||||
// @ts-ignore: The `_useHtmlParser2` internal option is the
|
||||
// simplest way to both parse and render using `htmlparser2`.
|
||||
_useHtmlParser2: true,
|
||||
decodeEntities: false,
|
||||
});
|
||||
|
||||
if (SettingsStore.getValue("feature_latex_maths")) {
|
||||
// original Markdown without LaTeX replacements
|
||||
const parserOrig = new Markdown(orig);
|
||||
const phtmlOrig = cheerio.load(parserOrig.toHTML(),
|
||||
{ _useHtmlParser2: true, decodeEntities: false });
|
||||
const phtmlOrig = cheerio.load(parserOrig.toHTML(), {
|
||||
// @ts-ignore: The `_useHtmlParser2` internal option is the
|
||||
// simplest way to both parse and render using `htmlparser2`.
|
||||
_useHtmlParser2: true,
|
||||
decodeEntities: false,
|
||||
});
|
||||
|
||||
// since maths delimiters are handled before Markdown,
|
||||
// code blocks could contain mangled content.
|
||||
|
@ -134,9 +143,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
|
|||
|
||||
// add fallback output for latex math, which should not be interpreted as markdown
|
||||
phtml('div, span').each(function(i, e) {
|
||||
const tex = phtml(e).attr('data-mx-maths')
|
||||
const tex = phtml(e).attr('data-mx-maths');
|
||||
if (tex) {
|
||||
phtml(e).html(`<code>${tex}</code>`)
|
||||
phtml(e).html(`<code>${tex}</code>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -148,31 +157,31 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} =
|
|||
}
|
||||
}
|
||||
|
||||
export function textSerialize(model: EditorModel) {
|
||||
export function textSerialize(model: EditorModel): string {
|
||||
return model.parts.reduce((text, part) => {
|
||||
switch (part.type) {
|
||||
case "newline":
|
||||
case Type.Newline:
|
||||
return text + "\n";
|
||||
case "plain":
|
||||
case "command":
|
||||
case "pill-candidate":
|
||||
case "at-room-pill":
|
||||
case Type.Plain:
|
||||
case Type.Command:
|
||||
case Type.PillCandidate:
|
||||
case Type.AtRoomPill:
|
||||
return text + part.text;
|
||||
case "room-pill":
|
||||
case Type.RoomPill:
|
||||
// Here we use the resourceId for compatibility with non-rich text clients
|
||||
// See https://github.com/vector-im/element-web/issues/16660
|
||||
return text + `${part.resourceId}`;
|
||||
case "user-pill":
|
||||
case Type.UserPill:
|
||||
return text + `${part.text}`;
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function containsEmote(model: EditorModel) {
|
||||
export function containsEmote(model: EditorModel): boolean {
|
||||
return startsWith(model, "/me ", false);
|
||||
}
|
||||
|
||||
export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) {
|
||||
export function startsWith(model: EditorModel, prefix: string, caseSensitive = true): boolean {
|
||||
const firstPart = model.parts[0];
|
||||
// part type will be "plain" while editing,
|
||||
// and "command" while composing a message.
|
||||
|
@ -182,28 +191,28 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t
|
|||
text = text.toLowerCase();
|
||||
}
|
||||
|
||||
return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix);
|
||||
return firstPart && (firstPart.type === Type.Plain || firstPart.type === Type.Command) && text.startsWith(prefix);
|
||||
}
|
||||
|
||||
export function stripEmoteCommand(model: EditorModel) {
|
||||
export function stripEmoteCommand(model: EditorModel): EditorModel {
|
||||
// trim "/me "
|
||||
return stripPrefix(model, "/me ");
|
||||
}
|
||||
|
||||
export function stripPrefix(model: EditorModel, prefix: string) {
|
||||
export function stripPrefix(model: EditorModel, prefix: string): EditorModel {
|
||||
model = model.clone();
|
||||
model.removeText({index: 0, offset: 0}, prefix.length);
|
||||
model.removeText({ index: 0, offset: 0 }, prefix.length);
|
||||
return model;
|
||||
}
|
||||
|
||||
export function unescapeMessage(model: EditorModel) {
|
||||
const {parts} = model;
|
||||
export function unescapeMessage(model: EditorModel): EditorModel {
|
||||
const { parts } = model;
|
||||
if (parts.length) {
|
||||
const firstPart = parts[0];
|
||||
// only unescape \/ to / at start of editor
|
||||
if (firstPart.type === "plain" && firstPart.text.startsWith("\\/")) {
|
||||
if (firstPart.type === Type.Plain && firstPart.text.startsWith("\\/")) {
|
||||
model = model.clone();
|
||||
model.removeText({index: 0, offset: 0}, 1);
|
||||
model.removeText({ index: 0, offset: 0 }, 1);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue