Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Добавил поддержку параметров для эмоджи #24

Merged
merged 2 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@ import render, { dispatch, isEmoji } from '../render';
import type { BaseEditorOptions, TextRange, Model } from './types';
import History, { HistoryEntry } from './history';
import { getTextRange, rangeToLocation, setDOMRange, setRange } from './range';
import { cutText, getText, insertText, removeText, replaceText, setFormat, toggleFormat, updateFromInputEvent, updateFromInputEventFallback, updateFromOldEvent } from './update';
import {
applyAnimojiParams,
cutText,
getText,
insertText,
removeText,
replaceText,
setFormat,
toggleFormat,
updateFromInputEvent,
updateFromInputEventFallback,
updateFromOldEvent
} from './update';
import { setLink, slice, mdToText, textToMd, getFormat } from '../formatted-string';
import type { TokenFormatUpdate, TextRange as Rng } from '../formatted-string';
import Shortcuts from './shortcuts';
Expand All @@ -14,6 +26,7 @@ import parseHTML from '../parser/html2';
import toHTML from '../render/html';
import { last } from '../parser/utils';
import { objectMerge } from '../utils/objectMerge';
import type { EmojiParams } from '../parser/types';

const enum DiffActionType {
Insert = 'insert',
Expand Down Expand Up @@ -412,6 +425,21 @@ export default class Editor {
return result;
}

/**
* Заменяет текст в указанном диапазоне `from:to` на один эмоджи, и записывает ему указанные параметры
* Если передать более одного эмоджи, указанные параметры будут записаны всем переданным эмоджи
*/
replaceOnEmoji(from: number, to: number, emoji: string, params?: EmojiParams): Model {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sergeche нужно ли тут проверить, что в emoji: string содержится только эмоджи без других символов?
Я тестировал, и этот метод отрабатывает нормально, когда передаем просто текст (например "123"), текст + эмоджи ("123😀"), несколько эмоджи ("😀😀", тогда параметры применятся ко всем переданным эмоджи)

emoji = this.sanitizeText(emoji);
let nextModel = replaceText(this.model, emoji, from, to, this.options);

// устанавливаем параметры анимоджи в диапазоне куда мы добавили эмоджи
nextModel = applyAnimojiParams(nextModel, from, from + emoji.length, params);

const pos = from + emoji.length;
return this.updateModel(nextModel, DiffActionType.Insert, [pos, pos]);
}

/**
* Вырезает фрагмент по указанному диапазону из модели и возвращает его
* @returns Вырезанный фрагмент модели
Expand Down
83 changes: 82 additions & 1 deletion src/editor/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {
cutText as plainCutText, setFormat as plainSetFormat, setLink, slice,
} from '../formatted-string';
import type { TokenFormatUpdate, CutText } from '../formatted-string';
import { isCustomLink, tokenForPos } from '../formatted-string/utils';
import { isCustomLink, tokenForPos, tokenRange } from '../formatted-string/utils';
import type { BaseEditorOptions, TextRange, Model } from './types';
import { getInputText, isCollapsed, startsWith } from './utils';
import type { EmojiParams } from '../parser/types';

const skipInputTypes = new Set<string>([
'insertOrderedList',
Expand Down Expand Up @@ -120,6 +121,10 @@ export function applyFormatFromFragment(model: Model, fragment: Model, offset =
model = setLink(model, token.link, offset, len);
}

if (token.emoji?.length) {
model = setEmojiParamsFromFragment(model, token, offset, len);
}

offset += len;
});

Expand Down Expand Up @@ -277,3 +282,79 @@ export function getInputEventText(evt: InputEvent): string {

return '';
}

/**
* Устанавливает параметры эмоджи из фрагмента
*/
export function setEmojiParamsFromFragment(tokens: Token[], fragment: Token, pos: number, posLen = 0): Token[] {

const [start, end] = tokenRange(tokens, pos, pos + posLen);

if (start.index === -1 || end.index === -1 || end.index < start.index) {
return tokens;
}

const startTokens = tokens.slice(0, start.index);
let midTokens = tokens.slice(start.index, end.index + 1);
const endTokens = tokens.slice(end.index + 1);

const offset = pos - getText(startTokens).length;

let len = 0;
midTokens = midTokens.map((token) => {
token = {
sergeche marked this conversation as resolved.
Show resolved Hide resolved
...token,
emoji: token.emoji?.map((emoji) => {
const fragmentEmoji = fragment.emoji?.find((item) => item.from + offset === len + emoji.from);

if (fragmentEmoji) {
return {
...emoji,
params: fragmentEmoji.params,
}
}
return emoji;
}),
};

len += token.value.length;
return token;
});

return [...startTokens, ...midTokens, ...endTokens];
}

/**
* Применяет параметры анимоджи ко всем эмоджи в указаном диапазоне
*/
export function applyAnimojiParams(model: Model, from: number, to: number, params: EmojiParams): Model {
const [start, end] = tokenRange(model, from, to);

if (start.index !== -1 && end.index !== -1) {
for (let tokenIndex = start.index; tokenIndex <= end.index; tokenIndex++) {
// если это токен между start-токеном и end-токеном, то проходимся по всему диапазону
let offsetFrom = 0;
if (tokenIndex === start.index && start.offset !== -1) {
offsetFrom = start.offset;
}

let offsetTo = model[tokenIndex].value.length;
if (tokenIndex === end.index && end.offset !== -1) {
offsetTo = end.offset;
}

model[tokenIndex].emoji = model[tokenIndex].emoji?.map((item) => {
if (item.from >= offsetFrom && item.to <= offsetTo) {
return {
...item,
params,
}
}

return item;
});
}
}

return model;
}
30 changes: 29 additions & 1 deletion src/formatted-string/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,35 @@ function updateTokens(tokens: Token[], value: string, from: number, to: number,

if (nextTokens.length) {
// Вставляем/заменяем фрагмент
nextTokens.forEach(t => t.format = startToken.format);
let len = 0;
nextTokens.forEach((token) => {
token.format = startToken.format;

// переносим параметры эмоджи из startToken (до start.offset) и endToken (после end.offset),
// так как после parse() эти параметры теряются
token.emoji = token.emoji?.map((emoji) => {
if (!len && emoji.from < start.offset) {
const startTokenEmoji = startToken.emoji?.find((item) => item.from === emoji.from);

if (startTokenEmoji) {
return {
...emoji,
params: startTokenEmoji?.params,
}
}
}

if (len + emoji.from >= textBound) {
const endTokenEmoji = endToken.emoji?.find((item) => textBound + item.from - end.offset === len + emoji.from);
return {
...emoji,
params: endTokenEmoji?.params,
}
}
return emoji;
});
len += token.value.length;
});

// Применяем форматирование из концевых токенов, но только если можем
// сделать это безопасно: применяем только для текста
Expand Down
6 changes: 6 additions & 0 deletions src/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ export interface TokenNewline extends TokenBase {
type: TokenType.Newline;
}

export interface EmojiParams {
[key: string]: number | string;
}

export interface Emoji {
/** Начало эмоджи в родительском токене */
from: number;
Expand All @@ -189,4 +193,6 @@ export interface Emoji {
* Используется для текстовых эмоджи (алиасов)
* */
emoji?: string;
/** Дополнительные параметры эмоджи */
params?: EmojiParams;
}
9 changes: 5 additions & 4 deletions src/render/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { isAutoLink, isCustomLink } from '../formatted-string/utils';
import type { Emoji, Token, TokenHashTag, TokenLink, TokenMention, TokenCommand, TokenText } from '../parser';
import { TokenFormat, TokenType } from '../parser';
import type { EmojiParams } from '../parser/types';
import { objectMerge } from '../utils/objectMerge';

type ClassFormat = [type: TokenFormat, value: string];
export type EmojiRender = (emoji: string | null, elem?: Element, rawEmoji?: string) => Element | void;
export type EmojiRender = (emoji: string | null, elem?: Element, rawEmoji?: string, emojiParams?: EmojiParams) => Element | void;

export interface RenderOptions {
/**
Expand Down Expand Up @@ -210,7 +211,7 @@ function renderText(token: Token, state: ReconcileState): void {
state.text(text);
}

state.emoji(emoji, rawEmoji);
state.emoji(emoji, rawEmoji, emojiToken.params);
offset = emojiToken.to;
});

Expand Down Expand Up @@ -319,11 +320,11 @@ class ReconcileState {
/**
* Ожидает элемент с указанным эмоджи, при необходимости создаёт или обновляет его
*/
emoji(actualEmoji: string, rawEmoji: string): Element | void {
emoji(actualEmoji: string, rawEmoji: string, emojiParams?: EmojiParams): Element | void {
const { emoji } = this.options;
const node = this.container.childNodes[this.pos];
const isCurEmoji = node ? isEmoji(node) : false;
const next = emoji(actualEmoji, isCurEmoji ? node as Element : null, rawEmoji);
const next = emoji(actualEmoji, isCurEmoji ? node as Element : null, rawEmoji, emojiParams);

if (next) {
if (node !== next) {
Expand Down
Loading