Enlarge emoji in composer (#7602)
This commit is contained in:
parent
b5d11336f7
commit
6806c2cdca
12 changed files with 128 additions and 42 deletions
|
@ -86,6 +86,11 @@ a.mx_Pill {
|
||||||
margin-right: 0.24rem;
|
margin-right: 0.24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Emoji {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Markdown_BOLD {
|
.mx_Markdown_BOLD {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,8 @@ limitations under the License.
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
|
// fixed line height to prevent emoji from being taller than text
|
||||||
|
line-height: $font-18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_SenderProfile {
|
> .mx_SenderProfile {
|
||||||
|
|
|
@ -233,11 +233,6 @@ $left-gutter: 64px;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_Emoji {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_EventTile_selected .mx_EventTile_line,
|
&.mx_EventTile_selected .mx_EventTile_line,
|
||||||
&:hover .mx_EventTile_line {
|
&:hover .mx_EventTile_line {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
@ -391,7 +386,7 @@ $left-gutter: 64px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_bigEmoji .mx_EventTile_Emoji {
|
.mx_EventTile_bigEmoji .mx_Emoji {
|
||||||
font-size: 48px !important;
|
font-size: 48px !important;
|
||||||
line-height: 57px;
|
line-height: 57px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
// fixed line height to prevent emoji from being taller than text
|
||||||
|
line-height: calc(1.2 * $font-14px);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
// don't grow wider than available space
|
// don't grow wider than available space
|
||||||
|
|
|
@ -89,9 +89,8 @@ const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)
|
||||||
* Uses a much, much simpler regex than emojibase's so will give false
|
* Uses a much, much simpler regex than emojibase's so will give false
|
||||||
* positives, but useful for fast-path testing strings to see if they
|
* positives, but useful for fast-path testing strings to see if they
|
||||||
* need emojification.
|
* need emojification.
|
||||||
* unicodeToImage uses this function.
|
|
||||||
*/
|
*/
|
||||||
function mightContainEmoji(str: string): boolean {
|
export function mightContainEmoji(str: string): boolean {
|
||||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,9 +411,9 @@ export interface IOptsReturnString extends IOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojiToHtmlSpan = (emoji: string) =>
|
const emojiToHtmlSpan = (emoji: string) =>
|
||||||
`<span class='mx_EventTile_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
|
`<span class='mx_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
|
||||||
const emojiToJsxSpan = (emoji: string, key: number) =>
|
const emojiToJsxSpan = (emoji: string, key: number) =>
|
||||||
<span key={key} className='mx_EventTile_Emoji' title={unicodeToShortcode(emoji)}>{ emoji }</span>;
|
<span key={key} className='mx_Emoji' title={unicodeToShortcode(emoji)}>{ emoji }</span>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps emojis in <span> to style them separately from the rest of message. Consecutive emojis (and modifiers) are wrapped
|
* Wraps emojis in <span> to style them separately from the rest of message. Consecutive emojis (and modifiers) are wrapped
|
||||||
|
|
|
@ -199,7 +199,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
|
|
||||||
// this returns the amount of added/removed characters during the replace
|
// this returns the amount of added/removed characters during the replace
|
||||||
// so the caret position can be adjusted.
|
// so the caret position can be adjusted.
|
||||||
return range.replace([partCreator.plain(data.unicode)]);
|
return range.replace([partCreator.emoji(data.unicode)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -831,7 +831,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
const caret = this.getCaret();
|
const caret = this.getCaret();
|
||||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
model.transform(() => {
|
model.transform(() => {
|
||||||
const addedLen = model.insert([partCreator.plain(text)], position);
|
const addedLen = model.insert(partCreator.plainWithEmoji(text), position);
|
||||||
return model.positionForOffset(caret.offset + addedLen, true);
|
return model.positionForOffset(caret.offset + addedLen, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,7 @@ export default class AutocompleteWrapperModel {
|
||||||
return [(this.partCreator as CommandPartCreator).command(text)];
|
return [(this.partCreator as CommandPartCreator).command(text)];
|
||||||
default:
|
default:
|
||||||
// used for emoji and other plain text completion replacement
|
// used for emoji and other plain text completion replacement
|
||||||
return [this.partCreator.plain(text)];
|
return this.partCreator.plainWithEmoji(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ export function getLineAndNodePosition(model: EditorModel, caretPosition: IPosit
|
||||||
offset = 0;
|
offset = 0;
|
||||||
} else {
|
} else {
|
||||||
// move caret out of uneditable part (into caret node, or empty line br) if needed
|
// move caret out of uneditable part (into caret node, or empty line br) if needed
|
||||||
({ nodeIndex, offset } = moveOutOfUneditablePart(parts, partIndex, nodeIndex, offset));
|
({ nodeIndex, offset } = moveOutOfUnselectablePart(parts, partIndex, nodeIndex, offset));
|
||||||
}
|
}
|
||||||
return { lineIndex, nodeIndex, offset };
|
return { lineIndex, nodeIndex, offset };
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||||
nodeIndex += 1;
|
nodeIndex += 1;
|
||||||
}
|
}
|
||||||
// only jump over caret node if we're not at our destination node already,
|
// only jump over caret node if we're not at our destination node already,
|
||||||
// as we'll assume in moveOutOfUneditablePart that nodeIndex
|
// as we'll assume in moveOutOfUnselectablePart that nodeIndex
|
||||||
// refers to the node corresponding to the part,
|
// refers to the node corresponding to the part,
|
||||||
// and not an adjacent caret node
|
// and not an adjacent caret node
|
||||||
if (i < partIndex) {
|
if (i < partIndex) {
|
||||||
|
@ -140,10 +140,10 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) {
|
||||||
return { lineIndex, nodeIndex };
|
return { lineIndex, nodeIndex };
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveOutOfUneditablePart(parts: Part[], partIndex: number, nodeIndex: number, offset: number) {
|
function moveOutOfUnselectablePart(parts: Part[], partIndex: number, nodeIndex: number, offset: number) {
|
||||||
// move caret before or after uneditable part
|
// move caret before or after unselectable part
|
||||||
const part = parts[partIndex];
|
const part = parts[partIndex];
|
||||||
if (part && !part.canEdit) {
|
if (part && !part.acceptsCaret) {
|
||||||
if (offset === 0) {
|
if (offset === 0) {
|
||||||
nodeIndex -= 1;
|
nodeIndex -= 1;
|
||||||
const prevPart = parts[partIndex - 1];
|
const prevPart = parts[partIndex - 1];
|
||||||
|
|
|
@ -29,7 +29,7 @@ function parseAtRoomMentions(text: string, partCreator: PartCreator): Part[] {
|
||||||
const parts: Part[] = [];
|
const parts: Part[] = [];
|
||||||
text.split(ATROOM).forEach((textPart, i, arr) => {
|
text.split(ATROOM).forEach((textPart, i, arr) => {
|
||||||
if (textPart.length) {
|
if (textPart.length) {
|
||||||
parts.push(partCreator.plain(textPart));
|
parts.push(...partCreator.plainWithEmoji(textPart));
|
||||||
}
|
}
|
||||||
// it's safe to never append @room after the last textPart
|
// it's safe to never append @room after the last textPart
|
||||||
// as split will report an empty string at the end if
|
// as split will report an empty string at the end if
|
||||||
|
@ -42,28 +42,28 @@ function parseAtRoomMentions(text: string, partCreator: PartCreator): Part[] {
|
||||||
return parts;
|
return parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLink(a: HTMLAnchorElement, partCreator: PartCreator): Part {
|
function parseLink(a: HTMLAnchorElement, partCreator: PartCreator): Part[] {
|
||||||
const { href } = a;
|
const { href } = a;
|
||||||
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
|
const resourceId = getPrimaryPermalinkEntity(href); // The room/user ID
|
||||||
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
|
const prefix = resourceId ? resourceId[0] : undefined; // First character of ID
|
||||||
switch (prefix) {
|
switch (prefix) {
|
||||||
case "@":
|
case "@":
|
||||||
return partCreator.userPill(a.textContent, resourceId);
|
return [partCreator.userPill(a.textContent, resourceId)];
|
||||||
case "#":
|
case "#":
|
||||||
return partCreator.roomPill(resourceId);
|
return [partCreator.roomPill(resourceId)];
|
||||||
default: {
|
default: {
|
||||||
if (href === a.textContent) {
|
if (href === a.textContent) {
|
||||||
return partCreator.plain(a.textContent);
|
return partCreator.plainWithEmoji(a.textContent);
|
||||||
} else {
|
} else {
|
||||||
return partCreator.plain(`[${a.textContent.replace(/[[\\\]]/g, c => "\\" + c)}](${href})`);
|
return partCreator.plainWithEmoji(`[${a.textContent.replace(/[[\\\]]/g, c => "\\" + c)}](${href})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseImage(img: HTMLImageElement, partCreator: PartCreator): Part {
|
function parseImage(img: HTMLImageElement, partCreator: PartCreator): Part[] {
|
||||||
const { src } = img;
|
const { src } = img;
|
||||||
return partCreator.plain(`![${img.alt.replace(/[[\\\]]/g, c => "\\" + c)}](${src})`);
|
return partCreator.plainWithEmoji(`![${img.alt.replace(/[[\\\]]/g, c => "\\" + c)}](${src})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCodeBlock(n: HTMLElement, partCreator: PartCreator): Part[] {
|
function parseCodeBlock(n: HTMLElement, partCreator: PartCreator): Part[] {
|
||||||
|
@ -79,7 +79,7 @@ function parseCodeBlock(n: HTMLElement, partCreator: PartCreator): Part[] {
|
||||||
}
|
}
|
||||||
const preLines = ("```" + language + "\n" + n.textContent + "```").split("\n");
|
const preLines = ("```" + language + "\n" + n.textContent + "```").split("\n");
|
||||||
preLines.forEach((l, i) => {
|
preLines.forEach((l, i) => {
|
||||||
parts.push(partCreator.plain(l));
|
parts.push(...partCreator.plainWithEmoji(l));
|
||||||
if (i < preLines.length - 1) {
|
if (i < preLines.length - 1) {
|
||||||
parts.push(partCreator.newline());
|
parts.push(partCreator.newline());
|
||||||
}
|
}
|
||||||
|
@ -126,21 +126,21 @@ function parseElement(
|
||||||
partCreator.newline(),
|
partCreator.newline(),
|
||||||
];
|
];
|
||||||
case "EM":
|
case "EM":
|
||||||
return partCreator.plain(`_${n.textContent}_`);
|
return partCreator.plainWithEmoji(`_${n.textContent}_`);
|
||||||
case "STRONG":
|
case "STRONG":
|
||||||
return partCreator.plain(`**${n.textContent}**`);
|
return partCreator.plainWithEmoji(`**${n.textContent}**`);
|
||||||
case "PRE":
|
case "PRE":
|
||||||
return parseCodeBlock(n, partCreator);
|
return parseCodeBlock(n, partCreator);
|
||||||
case "CODE":
|
case "CODE":
|
||||||
return partCreator.plain(`\`${n.textContent}\``);
|
return partCreator.plainWithEmoji(`\`${n.textContent}\``);
|
||||||
case "DEL":
|
case "DEL":
|
||||||
return partCreator.plain(`<del>${n.textContent}</del>`);
|
return partCreator.plainWithEmoji(`<del>${n.textContent}</del>`);
|
||||||
case "SUB":
|
case "SUB":
|
||||||
return partCreator.plain(`<sub>${n.textContent}</sub>`);
|
return partCreator.plainWithEmoji(`<sub>${n.textContent}</sub>`);
|
||||||
case "SUP":
|
case "SUP":
|
||||||
return partCreator.plain(`<sup>${n.textContent}</sup>`);
|
return partCreator.plainWithEmoji(`<sup>${n.textContent}</sup>`);
|
||||||
case "U":
|
case "U":
|
||||||
return partCreator.plain(`<u>${n.textContent}</u>`);
|
return partCreator.plainWithEmoji(`<u>${n.textContent}</u>`);
|
||||||
case "LI": {
|
case "LI": {
|
||||||
const BASE_INDENT = 4;
|
const BASE_INDENT = 4;
|
||||||
const depth = state.listDepth - 1;
|
const depth = state.listDepth - 1;
|
||||||
|
@ -171,9 +171,9 @@ function parseElement(
|
||||||
((SdkConfig.get()['latex_maths_delims'] || {})['inline'] || {})['right'] || "\\)" :
|
((SdkConfig.get()['latex_maths_delims'] || {})['inline'] || {})['right'] || "\\)" :
|
||||||
((SdkConfig.get()['latex_maths_delims'] || {})['display'] || {})['right'] || "\\]";
|
((SdkConfig.get()['latex_maths_delims'] || {})['display'] || {})['right'] || "\\]";
|
||||||
const tex = n.getAttribute("data-mx-maths");
|
const tex = n.getAttribute("data-mx-maths");
|
||||||
return partCreator.plain(delimLeft + tex + delimRight);
|
return partCreator.plainWithEmoji(delimLeft + tex + delimRight);
|
||||||
} else if (!checkDescendInto(n)) {
|
} else if (!checkDescendInto(n)) {
|
||||||
return partCreator.plain(n.textContent);
|
return partCreator.plainWithEmoji(n.textContent);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ function parseElement(
|
||||||
default:
|
default:
|
||||||
// don't textify block nodes we'll descend into
|
// don't textify block nodes we'll descend into
|
||||||
if (!checkDescendInto(n)) {
|
if (!checkDescendInto(n)) {
|
||||||
return partCreator.plain(n.textContent);
|
return partCreator.plainWithEmoji(n.textContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { split } from "lodash";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -24,12 +25,13 @@ import AutocompleteWrapperModel, {
|
||||||
UpdateCallback,
|
UpdateCallback,
|
||||||
UpdateQuery,
|
UpdateQuery,
|
||||||
} from "./autocomplete";
|
} from "./autocomplete";
|
||||||
|
import { mightContainEmoji, unicodeToShortcode } from "../HtmlUtils";
|
||||||
import * as Avatar from "../Avatar";
|
import * as Avatar from "../Avatar";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
|
|
||||||
interface ISerializedPart {
|
interface ISerializedPart {
|
||||||
type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate;
|
type: Type.Plain | Type.Newline | Type.Emoji | Type.Command | Type.PillCandidate;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +46,7 @@ export type SerializedPart = ISerializedPart | ISerializedPillPart;
|
||||||
export enum Type {
|
export enum Type {
|
||||||
Plain = "plain",
|
Plain = "plain",
|
||||||
Newline = "newline",
|
Newline = "newline",
|
||||||
|
Emoji = "emoji",
|
||||||
Command = "command",
|
Command = "command",
|
||||||
UserPill = "user-pill",
|
UserPill = "user-pill",
|
||||||
RoomPill = "room-pill",
|
RoomPill = "room-pill",
|
||||||
|
@ -53,8 +56,9 @@ export enum Type {
|
||||||
|
|
||||||
interface IBasePart {
|
interface IBasePart {
|
||||||
text: string;
|
text: string;
|
||||||
type: Type.Plain | Type.Newline;
|
type: Type.Plain | Type.Newline | Type.Emoji;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
acceptsCaret: boolean;
|
||||||
|
|
||||||
createAutoComplete(updateCallback: UpdateCallback): void;
|
createAutoComplete(updateCallback: UpdateCallback): void;
|
||||||
|
|
||||||
|
@ -165,6 +169,10 @@ abstract class BasePart {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get acceptsCaret(): boolean {
|
||||||
|
return this.canEdit;
|
||||||
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return `${this.type}(${this.text})`;
|
return `${this.type}(${this.text})`;
|
||||||
}
|
}
|
||||||
|
@ -183,7 +191,7 @@ abstract class BasePart {
|
||||||
|
|
||||||
abstract class PlainBasePart extends BasePart {
|
abstract class PlainBasePart extends BasePart {
|
||||||
protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean {
|
protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean {
|
||||||
if (chr === "\n") {
|
if (chr === "\n" || mightContainEmoji(chr)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// when not pasting or dropping text, reject characters that should start a pill candidate
|
// when not pasting or dropping text, reject characters that should start a pill candidate
|
||||||
|
@ -351,6 +359,48 @@ class NewlinePart extends BasePart implements IBasePart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmojiPart extends BasePart implements IBasePart {
|
||||||
|
protected acceptsInsertion(chr: string, offset: number): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected acceptsRemoval(position: number, chr: string): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toDOMNode(): Node {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.className = "mx_Emoji";
|
||||||
|
span.setAttribute("title", unicodeToShortcode(this.text));
|
||||||
|
span.appendChild(document.createTextNode(this.text));
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateDOMNode(node: HTMLElement): void {
|
||||||
|
const textNode = node.childNodes[0];
|
||||||
|
if (textNode.textContent !== this.text) {
|
||||||
|
node.setAttribute("title", unicodeToShortcode(this.text));
|
||||||
|
textNode.textContent = this.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public canUpdateDOMNode(node: HTMLElement): boolean {
|
||||||
|
return node.className === "mx_Emoji";
|
||||||
|
}
|
||||||
|
|
||||||
|
public get type(): IBasePart["type"] {
|
||||||
|
return Type.Emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get canEdit(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get acceptsCaret(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RoomPillPart extends PillPart {
|
class RoomPillPart extends PillPart {
|
||||||
constructor(resourceId: string, label: string, private room: Room) {
|
constructor(resourceId: string, label: string, private room: Room) {
|
||||||
super(resourceId, label);
|
super(resourceId, label);
|
||||||
|
@ -503,6 +553,9 @@ export class PartCreator {
|
||||||
case "\n":
|
case "\n":
|
||||||
return new NewlinePart();
|
return new NewlinePart();
|
||||||
default:
|
default:
|
||||||
|
if (mightContainEmoji(input[0])) {
|
||||||
|
return new EmojiPart();
|
||||||
|
}
|
||||||
return new PlainPart();
|
return new PlainPart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -517,6 +570,8 @@ export class PartCreator {
|
||||||
return this.plain(part.text);
|
return this.plain(part.text);
|
||||||
case Type.Newline:
|
case Type.Newline:
|
||||||
return this.newline();
|
return this.newline();
|
||||||
|
case Type.Emoji:
|
||||||
|
return this.emoji(part.text);
|
||||||
case Type.AtRoomPill:
|
case Type.AtRoomPill:
|
||||||
return this.atRoomPill(part.text);
|
return this.atRoomPill(part.text);
|
||||||
case Type.PillCandidate:
|
case Type.PillCandidate:
|
||||||
|
@ -536,6 +591,10 @@ export class PartCreator {
|
||||||
return new NewlinePart("\n");
|
return new NewlinePart("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public emoji(text: string): EmojiPart {
|
||||||
|
return new EmojiPart(text);
|
||||||
|
}
|
||||||
|
|
||||||
public pillCandidate(text: string): PillCandidatePart {
|
public pillCandidate(text: string): PillCandidatePart {
|
||||||
return new PillCandidatePart(text, this.autoCompleteCreator);
|
return new PillCandidatePart(text, this.autoCompleteCreator);
|
||||||
}
|
}
|
||||||
|
@ -562,6 +621,28 @@ export class PartCreator {
|
||||||
return new UserPillPart(userId, displayName, member);
|
return new UserPillPart(userId, displayName, member);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public plainWithEmoji(text: string): (PlainPart | EmojiPart)[] {
|
||||||
|
const parts = [];
|
||||||
|
let plainText = "";
|
||||||
|
|
||||||
|
// We use lodash's grapheme splitter to avoid breaking apart compound emojis
|
||||||
|
for (const char of split(text, "")) {
|
||||||
|
if (mightContainEmoji(char)) {
|
||||||
|
if (plainText) {
|
||||||
|
parts.push(this.plain(plainText));
|
||||||
|
plainText = "";
|
||||||
|
}
|
||||||
|
parts.push(this.emoji(char));
|
||||||
|
} else {
|
||||||
|
plainText += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (plainText) {
|
||||||
|
parts.push(this.plain(plainText));
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
public createMentionParts(
|
public createMentionParts(
|
||||||
insertTrailingCharacter: boolean,
|
insertTrailingCharacter: boolean,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
|
|
|
@ -20,11 +20,11 @@ import EditorModel from "./model";
|
||||||
|
|
||||||
export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean {
|
export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean {
|
||||||
const isFirst = !prevPart || prevPart.type === Type.Newline;
|
const isFirst = !prevPart || prevPart.type === Type.Newline;
|
||||||
return !part.canEdit && (isFirst || !prevPart.canEdit);
|
return !part.acceptsCaret && (isFirst || !prevPart.acceptsCaret);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean {
|
export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean {
|
||||||
return !part.canEdit && isLastOfLine;
|
return !part.acceptsCaret && isLastOfLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void {
|
function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export function mdSerialize(model: EditorModel): string {
|
||||||
case Type.Newline:
|
case Type.Newline:
|
||||||
return html + "\n";
|
return html + "\n";
|
||||||
case Type.Plain:
|
case Type.Plain:
|
||||||
|
case Type.Emoji:
|
||||||
case Type.Command:
|
case Type.Command:
|
||||||
case Type.PillCandidate:
|
case Type.PillCandidate:
|
||||||
case Type.AtRoomPill:
|
case Type.AtRoomPill:
|
||||||
|
@ -164,6 +165,7 @@ export function textSerialize(model: EditorModel): string {
|
||||||
case Type.Newline:
|
case Type.Newline:
|
||||||
return text + "\n";
|
return text + "\n";
|
||||||
case Type.Plain:
|
case Type.Plain:
|
||||||
|
case Type.Emoji:
|
||||||
case Type.Command:
|
case Type.Command:
|
||||||
case Type.PillCandidate:
|
case Type.PillCandidate:
|
||||||
case Type.AtRoomPill:
|
case Type.AtRoomPill:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue