Browse Source

List navigation for stickers helper

master
Eduard Kuzmenko 4 years ago
parent
commit
197da96325
  1. 32
      src/components/chat/autocompleteHelper.ts
  2. 46
      src/components/chat/input.ts
  3. 2
      src/components/chat/markupTooltip.ts
  4. 47
      src/components/chat/stickersHelper.ts
  5. 10
      src/components/emoticonsDropdown/index.ts
  6. 4
      src/components/inputField.ts
  7. 10
      src/components/popups/createPoll.ts
  8. 160
      src/helpers/dom/attachListNavigation.ts
  9. 123
      src/helpers/dom/getRichElementValue.ts
  10. 121
      src/helpers/dom/getRichValue.ts
  11. 52
      src/helpers/dom/getRichValueWithCaret.ts
  12. 2
      src/helpers/dom/isInputEmpty.ts
  13. 2
      src/lib/appManagers/appPollsManager.ts
  14. 28
      src/scss/partials/_autocompleteHelper.scss
  15. 37
      src/scss/partials/_chatStickersHelper.scss
  16. 8
      src/scss/style.scss

32
src/components/chat/autocompleteHelper.ts

@ -0,0 +1,32 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import EventListenerBase from "../../helpers/eventListenerBase";
import rootScope from "../../lib/rootScope";
import SetTransition from "../singleTransition";
export default class AutocompleteHelper extends EventListenerBase<{
hidden: () => void,
visible: () => void,
}> {
protected container: HTMLElement;
constructor(appendTo: HTMLElement) {
super(false);
this.container = document.createElement('div');
this.container.classList.add('autocomplete-helper', 'z-depth-1');
appendTo.append(this.container);
}
public toggle(hide?: boolean) {
hide = hide === undefined ? this.container.classList.contains('is-visible') : hide;
SetTransition(this.container, 'is-visible', !hide, rootScope.settings.animationsEnabled ? 200 : 0, () => {
this.dispatchEvent(hide ? 'hidden' : 'visible');
});
}
}

46
src/components/chat/input.ts

@ -51,10 +51,13 @@ import blurActiveElement from '../../helpers/dom/blurActiveElement';
import { cancelEvent } from '../../helpers/dom/cancelEvent'; import { cancelEvent } from '../../helpers/dom/cancelEvent';
import cancelSelection from '../../helpers/dom/cancelSelection'; import cancelSelection from '../../helpers/dom/cancelSelection';
import { attachClickEvent } from '../../helpers/dom/clickEvent'; import { attachClickEvent } from '../../helpers/dom/clickEvent';
import getRichValue, { MarkdownType, markdownTags } from '../../helpers/dom/getRichValue'; import getRichValue from '../../helpers/dom/getRichValue';
import isInputEmpty from '../../helpers/dom/isInputEmpty'; import isInputEmpty from '../../helpers/dom/isInputEmpty';
import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed'; import isSendShortcutPressed from '../../helpers/dom/isSendShortcutPressed';
import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd'; import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
import { MarkdownType, markdownTags } from '../../helpers/dom/getRichElementValue';
import getRichValueWithCaret from '../../helpers/dom/getRichValueWithCaret';
import searchIndexManager from '../../lib/searchIndexManager';
const RECORD_MIN_TIME = 500; const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -62,6 +65,7 @@ const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export default class ChatInput { export default class ChatInput {
public static AUTO_COMPLETE_REG_EXP = /(\s|^)(:|@|\/)([\S]*)$/;
public pageEl = document.getElementById('page-chats') as HTMLDivElement; public pageEl = document.getElementById('page-chats') as HTMLDivElement;
public messageInput: HTMLElement; public messageInput: HTMLElement;
public messageInputField: InputField; public messageInputField: InputField;
@ -137,6 +141,8 @@ export default class ChatInput {
public fakeRowsWrapper: HTMLDivElement; public fakeRowsWrapper: HTMLDivElement;
private fakePinnedControlBtn: HTMLElement; private fakePinnedControlBtn: HTMLElement;
public previousQuery: string;
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager, private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager, private appNotificationsManager: AppNotificationsManager) { constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager, private appDraftsManager: AppDraftsManager, private serverTimeManager: ServerTimeManager, private appNotificationsManager: AppNotificationsManager) {
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
} }
@ -595,15 +601,14 @@ export default class ChatInput {
public saveDraft() { public saveDraft() {
if(!this.chat.peerId || this.editMsgId || this.chat.type === 'scheduled') return; if(!this.chat.peerId || this.editMsgId || this.chat.type === 'scheduled') return;
const entities: MessageEntity[] = []; const {value, entities} = getRichValue(this.messageInputField.input);
const str = getRichValue(this.messageInputField.input, entities);
let draft: DraftMessage.draftMessage; let draft: DraftMessage.draftMessage;
if(str.length || this.replyToMsgId) { if(value.length || this.replyToMsgId) {
draft = { draft = {
_: 'draftMessage', _: 'draftMessage',
date: tsNow(true) + this.serverTimeManager.serverTimeOffset, date: tsNow(true) + this.serverTimeManager.serverTimeOffset,
message: str, message: value,
entities: entities.length ? entities : undefined, entities: entities.length ? entities : undefined,
pFlags: { pFlags: {
no_webpage: this.noWebPage no_webpage: this.noWebPage
@ -1021,35 +1026,27 @@ export default class ChatInput {
//console.log('messageInput input', this.messageInput.innerText); //console.log('messageInput input', this.messageInput.innerText);
//const value = this.messageInput.innerText; //const value = this.messageInput.innerText;
const markdownEntities: MessageEntity[] = []; const {value: richValue, entities: markdownEntities, caretPos} = getRichValueWithCaret(this.messageInputField.input);
const richValue = getRichValue(this.messageInputField.input, markdownEntities);
//const entities = RichTextProcessor.parseEntities(value); //const entities = RichTextProcessor.parseEntities(value);
const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities); const value = RichTextProcessor.parseMarkdown(richValue, markdownEntities);
const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value)); const entities = RichTextProcessor.mergeEntities(markdownEntities, RichTextProcessor.parseEntities(value));
//this.chat.log('messageInput entities', richValue, value, markdownEntities); this.chat.log('messageInput entities', richValue, value, markdownEntities, caretPos);
if(this.stickersHelper && if(this.stickersHelper &&
rootScope.settings.stickers.suggest && rootScope.settings.stickers.suggest &&
(this.chat.peerId > 0 || this.appChatsManager.hasRights(this.chat.peerId, 'send_stickers'))) { (this.chat.peerId > 0 || this.appChatsManager.hasRights(this.chat.peerId, 'send_stickers'))) {
let emoticon = ''; let emoticon = '';
if(entities.length && entities[0]._ === 'messageEntityEmoji') { const entity = entities[0];
const entity = entities[0]; if(entity?._ === 'messageEntityEmoji' && entity.length === richValue.length && !entity.offset) {
if(entity.length === richValue.length && !entity.offset) { emoticon = richValue;
emoticon = richValue;
}
} }
this.stickersHelper.checkEmoticon(emoticon); this.stickersHelper.checkEmoticon(emoticon);
} }
if(!richValue.trim()) { if(this.canRedoFromHTML && !this.lockRedo && this.messageInput.innerHTML !== this.canRedoFromHTML) {
this.appImManager.markupTooltip.hide();
}
const html = this.messageInput.innerHTML;
if(this.canRedoFromHTML && html !== this.canRedoFromHTML && !this.lockRedo) {
this.canRedoFromHTML = ''; this.canRedoFromHTML = '';
this.undoHistory.length = 0; this.undoHistory.length = 0;
} }
@ -1104,10 +1101,12 @@ export default class ChatInput {
} }
} }
if(this.isInputEmpty()) { if(!richValue.trim()) {
if(this.lastTimeType) { if(this.lastTimeType) {
this.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageCancelAction'}); this.appMessagesManager.setTyping(this.chat.peerId, {_: 'sendMessageCancelAction'});
} }
this.appImManager.markupTooltip.hide();
} else { } else {
const time = Date.now(); const time = Date.now();
if(time - this.lastTimeType >= 6000) { if(time - this.lastTimeType >= 6000) {
@ -1346,17 +1345,16 @@ export default class ChatInput {
return; return;
} }
const entities: MessageEntity[] = []; const {value, entities} = getRichValue(this.messageInputField.input);
const str = getRichValue(this.messageInputField.input, entities);
//return; //return;
if(this.editMsgId) { if(this.editMsgId) {
this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), str, { this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), value, {
entities, entities,
noWebPage: this.noWebPage noWebPage: this.noWebPage
}); });
} else { } else {
this.appMessagesManager.sendText(this.chat.peerId, str, { this.appMessagesManager.sendText(this.chat.peerId, value, {
entities, entities,
replyToMsgId: this.replyToMsgId, replyToMsgId: this.replyToMsgId,
threadId: this.chat.threadId, threadId: this.chat.threadId,

2
src/components/chat/markupTooltip.ts

@ -14,9 +14,9 @@ import appNavigationController from "../appNavigationController";
import { _i18n } from "../../lib/langPack"; import { _i18n } from "../../lib/langPack";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent";
import { MarkdownType, markdownTags } from "../../helpers/dom/getRichValue";
import getSelectedNodes from "../../helpers/dom/getSelectedNodes"; import getSelectedNodes from "../../helpers/dom/getSelectedNodes";
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
import { MarkdownType, markdownTags } from "../../helpers/dom/getRichElementValue";
//import { logger } from "../../lib/logger"; //import { logger } from "../../lib/logger";
export default class MarkupTooltip { export default class MarkupTooltip {

47
src/components/chat/stickersHelper.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import findUpClassName from "../../helpers/dom/findUpClassName"; import attachListNavigation from "../../helpers/dom/attachlistNavigation";
import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { MyDocument } from "../../lib/appManagers/appDocsManager";
import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import appStickersManager from "../../lib/appManagers/appStickersManager"; import appStickersManager from "../../lib/appManagers/appStickersManager";
@ -12,21 +12,36 @@ import { EmoticonsDropdown } from "../emoticonsDropdown";
import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers"; import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers";
import LazyLoadQueue from "../lazyLoadQueue"; import LazyLoadQueue from "../lazyLoadQueue";
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import SetTransition from "../singleTransition"; import AutocompleteHelper from "./autocompleteHelper";
export default class StickersHelper { export default class StickersHelper extends AutocompleteHelper {
private container: HTMLElement;
private stickersContainer: HTMLElement; private stickersContainer: HTMLElement;
private scrollable: Scrollable; private scrollable: Scrollable;
private superStickerRenderer: SuperStickerRenderer; private superStickerRenderer: SuperStickerRenderer;
private lazyLoadQueue: LazyLoadQueue; private lazyLoadQueue: LazyLoadQueue;
private lastEmoticon = ''; private lastEmoticon = '';
constructor(private appendTo: HTMLElement) { constructor(appendTo: HTMLElement) {
this.container = document.createElement('div'); super(appendTo);
this.container.classList.add('stickers-helper', 'z-depth-1');
this.appendTo.append(this.container); this.container.classList.add('stickers-helper');
this.addEventListener('visible', () => {
const list = this.stickersContainer;
const {detach} = attachListNavigation({
list,
type: 'xy',
onSelect: (target) => {
EmoticonsDropdown.onMediaClick({target}, true);
},
once: true
});
this.addEventListener('hidden', () => {
list.innerHTML = '';
detach();
}, true);
});
} }
public checkEmoticon(emoticon: string) { public checkEmoticon(emoticon: string) {
@ -34,11 +49,7 @@ export default class StickersHelper {
if(this.lastEmoticon && !emoticon) { if(this.lastEmoticon && !emoticon) {
if(this.container) { if(this.container) {
SetTransition(this.container, 'is-visible', false, 200, () => { this.toggle(true);
if(this.stickersContainer) {
this.stickersContainer.innerHTML = '';
}
});
} }
} }
@ -84,21 +95,13 @@ export default class StickersHelper {
this.stickersContainer.replaceWith(container); this.stickersContainer.replaceWith(container);
this.stickersContainer = container; this.stickersContainer = container;
SetTransition(this.container, 'is-visible', !!stickers.length, 200); this.toggle(!stickers.length);
this.scrollable.scrollTop = 0; this.scrollable.scrollTop = 0;
}); });
}); });
} }
private init() { private init() {
this.container.addEventListener('click', (e) => {
if(!findUpClassName(e.target, 'super-sticker')) {
return;
}
EmoticonsDropdown.onMediaClick(e, true);
});
this.stickersContainer = document.createElement('div'); this.stickersContainer = document.createElement('div');
this.stickersContainer.classList.add('stickers-helper-stickers', 'super-stickers'); this.stickersContainer.classList.add('stickers-helper-stickers', 'super-stickers');

10
src/components/emoticonsDropdown/index.ts

@ -394,7 +394,7 @@ export class EmoticonsDropdown {
return stickyIntersector; return stickyIntersector;
}; };
public static onMediaClick = (e: MouseEvent, clearDraft = false) => { public static onMediaClick = (e: {target: EventTarget | Element}, clearDraft = false) => {
let target = e.target as HTMLElement; let target = e.target as HTMLElement;
target = findUpTag(target, 'DIV'); target = findUpTag(target, 'DIV');
@ -406,9 +406,11 @@ export class EmoticonsDropdown {
if(appImManager.chat.input.sendMessageWithDocument(fileId, undefined, clearDraft)) { if(appImManager.chat.input.sendMessageWithDocument(fileId, undefined, clearDraft)) {
/* dropdown.classList.remove('active'); /* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */ toggleEl.classList.remove('active'); */
emoticonsDropdown.forceClose = true; if(emoticonsDropdown.container) {
emoticonsDropdown.container.classList.add('disable-hover'); emoticonsDropdown.forceClose = true;
emoticonsDropdown.toggle(false); emoticonsDropdown.container.classList.add('disable-hover');
emoticonsDropdown.toggle(false);
}
} else { } else {
console.warn('got no doc by id:', fileId); console.warn('got no doc by id:', fileId);
} }

4
src/components/inputField.ts

@ -182,7 +182,7 @@ class InputField {
processInput = () => { processInput = () => {
const wasError = input.classList.contains('error'); const wasError = input.classList.contains('error');
// * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol // * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol
const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input)].length; const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input, false).value].length;
const diff = maxLength - inputLength; const diff = maxLength - inputLength;
const isError = diff < 0; const isError = diff < 0;
input.classList.toggle('error', isError); input.classList.toggle('error', isError);
@ -232,7 +232,7 @@ class InputField {
} }
get value() { get value() {
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input); return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input, false).value;
//return getRichValue(this.input); //return getRichValue(this.input);
} }

10
src/components/popups/createPoll.ts

@ -185,7 +185,7 @@ export default class PopupCreatePoll extends PopupElement {
private getFilledAnswers() { private getFilledAnswers() {
const answers = Array.from(this.questions.children).map((el, idx) => { const answers = Array.from(this.questions.children).map((el, idx) => {
const input = el.querySelector('.input-field-input') as HTMLElement; const input = el.querySelector('.input-field-input') as HTMLElement;
return input instanceof HTMLInputElement ? input.value : getRichValue(input); return input instanceof HTMLInputElement ? input.value : getRichValue(input, false).value;
}).filter(v => !!v.trim()); }).filter(v => !!v.trim());
return answers; return answers;
@ -219,9 +219,8 @@ export default class PopupCreatePoll extends PopupElement {
return false; return false;
} }
const quizSolutionEntities: MessageEntity[] = []; const {value: quizSolution} = getRichValue(this.quizSolutionField.input, false);
const quizSolution = getRichValue(this.quizSolutionField.input, quizSolutionEntities) || undefined; if(quizSolution.length > MAX_LENGTH_SOLUTION) {
if(quizSolution?.length > MAX_LENGTH_SOLUTION) {
return false; return false;
} }
@ -238,8 +237,7 @@ export default class PopupCreatePoll extends PopupElement {
const answers = this.getFilledAnswers(); const answers = this.getFilledAnswers();
const quizSolutionEntities: MessageEntity[] = []; const {value: quizSolution, entities: quizSolutionEntities} = getRichValue(this.quizSolutionField.input);
const quizSolution = getRichValue(this.quizSolutionField.input, quizSolutionEntities) || undefined;
if(this.chat.type === 'scheduled' && !force) { if(this.chat.type === 'scheduled' && !force) {
this.chat.input.scheduleSending(() => { this.chat.input.scheduleSending(() => {

160
src/helpers/dom/attachListNavigation.ts

@ -0,0 +1,160 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import fastSmoothScroll from "../fastSmoothScroll";
import { cancelEvent } from "./cancelEvent";
import { attachClickEvent, detachClickEvent } from "./clickEvent";
import findUpAsChild from "./findUpAsChild";
import findUpClassName from "./findUpClassName";
type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
const HANDLE_EVENT = 'keydown';
const ACTIVE_CLASS_NAME = 'active';
export default function attachListNavigation({list, type, onSelect, once}: {
list: HTMLElement,
type: 'xy' | 'x' | 'y',
onSelect: (target: Element) => void | boolean,
once: boolean,
}) {
const keyNames: Set<ArrowKey> = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']);
let target: Element;
const getCurrentTarget = () => {
return target || list.querySelector('.' + ACTIVE_CLASS_NAME) || list.firstElementChild;
};
const setCurrentTarget = (_target: Element) => {
if(target === _target) {
return;
}
let hadTarget = false;
if(target) {
hadTarget = true;
target.classList.remove(ACTIVE_CLASS_NAME);
}
target = _target;
target.classList.add(ACTIVE_CLASS_NAME);
if(hadTarget && scrollable) {
fastSmoothScroll(scrollable, target as HTMLElement, 'center', undefined, undefined, undefined, 100, type === 'x' ? 'x' : 'y');
}
};
const getNextTargetX = (currentTarget: Element, isNext: boolean) => {
let nextTarget: Element;
if(isNext) nextTarget = currentTarget.nextElementSibling || list.firstElementChild;
else nextTarget = currentTarget.previousElementSibling || list.lastElementChild;
return nextTarget;
};
const getNextTargetY = (currentTarget: Element, isNext: boolean) => {
const property = isNext ? 'nextElementSibling' : 'previousElementSibling';
const endProperty = isNext ? 'firstElementChild' : 'lastElementChild';
const currentRect = currentTarget.getBoundingClientRect();
let nextTarget = currentTarget[property] || list[endProperty];
while(nextTarget !== currentTarget) {
const targetRect = nextTarget.getBoundingClientRect();
if(targetRect.x === currentRect.x && targetRect.y !== currentRect.y) {
break;
}
nextTarget = nextTarget[property] || list[endProperty];
}
return nextTarget;
};
let handleArrowKey: (currentTarget: Element, key: ArrowKey) => Element;
if(type === 'xy') { // flex-direction: row; flex-wrap: wrap;
handleArrowKey = (currentTarget, key) => {
if(key === 'ArrowUp' || key === 'ArrowDown') return getNextTargetY(currentTarget, key === 'ArrowDown');
else return getNextTargetX(currentTarget, key === 'ArrowRight');
};
} else { // flex-direction: row | column;
handleArrowKey = (currentTarget, key) => getNextTargetX(currentTarget, key === 'ArrowRight' || key === 'ArrowDown');
}
const onKeyDown = (e: KeyboardEvent) => {
if(!keyNames.has(e.key as any)) {
if(e.key === 'Enter') {
cancelEvent(e);
fireSelect(getCurrentTarget());
}
return;
}
cancelEvent(e);
if(list.childElementCount > 1) {
let currentTarget = getCurrentTarget();
currentTarget = handleArrowKey(currentTarget, e.key as any);
setCurrentTarget(currentTarget);
}
return false;
};
const scrollable = findUpClassName(list, 'scrollable');
list.classList.add('navigable-list');
const onMouseMove = (e: MouseEvent) => {
const target = findUpAsChild(e.target, list) as HTMLElement;
if(!target) {
return;
}
setCurrentTarget(target);
};
const onClick = (e: Event) => {
cancelEvent(e); // cancel keyboard closening
const target = findUpAsChild(e.target, list) as HTMLElement;
if(!target) {
return;
}
setCurrentTarget(target);
fireSelect(getCurrentTarget());
};
const fireSelect = (target: Element) => {
const canContinue = onSelect(target);
if(canContinue !== undefined ? !canContinue : once) {
detach();
}
};
const detach = () => {
// input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
list.removeEventListener('mousemove', onMouseMove);
detachClickEvent(list, onClick);
};
const resetTarget = () => {
setCurrentTarget(list.firstElementChild);
};
resetTarget();
// const input = document.activeElement as HTMLElement;
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
list.addEventListener('mousemove', onMouseMove, {passive: true});
attachClickEvent(list, onClick);
return {
detach,
resetTarget
};
}

123
src/helpers/dom/getRichElementValue.ts

@ -0,0 +1,123 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { MessageEntity } from "../../layer";
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link';
export type MarkdownTag = {
match: string,
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl';
};
export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
bold: {
match: '[style*="font-weight"], b',
entityName: 'messageEntityBold'
},
underline: {
match: '[style*="underline"], u',
entityName: 'messageEntityUnderline'
},
italic: {
match: '[style*="italic"], i',
entityName: 'messageEntityItalic'
},
monospace: {
match: '[style*="monospace"], [face="monospace"]',
entityName: 'messageEntityPre'
},
strikethrough: {
match: '[style*="line-through"], strike',
entityName: 'messageEntityStrike'
},
link: {
match: 'A',
entityName: 'messageEntityTextUrl'
}
};
export default function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) {
if(node.nodeType === 3) { // TEXT
if(selNode === node) {
const value = node.nodeValue;
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset));
} else {
const nodeValue = node.nodeValue;
line.push(nodeValue);
if(entities && nodeValue.trim()) {
if(node.parentNode) {
const parentElement = node.parentElement;
for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType];
const closest = parentElement.closest(tag.match + ', [contenteditable]');
if(closest && closest.getAttribute('contenteditable') === null) {
if(tag.entityName === 'messageEntityTextUrl') {
entities.push({
_: tag.entityName as any,
url: (parentElement as HTMLAnchorElement).href,
offset: offset.offset,
length: nodeValue.length
});
} else {
entities.push({
_: tag.entityName as any,
offset: offset.offset,
length: nodeValue.length
});
}
}
}
}
}
offset.offset += nodeValue.length;
}
return;
}
if(node.nodeType !== 1) { // NON-ELEMENT
return;
}
const isSelected = (selNode === node);
const isBlock = node.tagName === 'DIV' || node.tagName === 'P';
if(isBlock && line.length || node.tagName === 'BR') {
lines.push(line.join(''));
line.splice(0, line.length);
} else if(node.tagName === 'IMG') {
const alt = (node as HTMLImageElement).alt;
if(alt) {
line.push(alt);
offset.offset += alt.length;
}
}
if(isSelected && !selOffset) {
line.push('\x01');
}
let curChild = node.firstChild as HTMLElement;
while(curChild) {
getRichElementValue(curChild, lines, line, selNode, selOffset, entities, offset);
curChild = curChild.nextSibling as any;
}
if(isSelected && selOffset) {
line.push('\x01');
}
if(isBlock && line.length) {
lines.push(line.join(''));
line.splice(0, line.length);
}
}

121
src/helpers/dom/getRichValue.ts

@ -12,15 +12,13 @@
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { MessageEntity } from "../../layer"; import { MessageEntity } from "../../layer";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import getRichElementValue from "./getRichElementValue";
export default function getRichValue(field: HTMLElement, entities?: MessageEntity[]) { export default function getRichValue(field: HTMLElement, withEntities = true) {
if(!field) {
return '';
}
const lines: string[] = []; const lines: string[] = [];
const line: string[] = []; const line: string[] = [];
const entities: MessageEntity[] = withEntities ? [] : undefined;
getRichElementValue(field, lines, line, undefined, undefined, entities); getRichElementValue(field, lines, line, undefined, undefined, entities);
if(line.length) { if(line.length) {
lines.push(line.join('')); lines.push(line.join(''));
@ -35,118 +33,7 @@ export default function getRichValue(field: HTMLElement, entities?: MessageEntit
//console.log('getRichValue:', value, entities); //console.log('getRichValue:', value, entities);
return value; return {value, entities};
} }
MOUNT_CLASS_TO.getRichValue = getRichValue; MOUNT_CLASS_TO.getRichValue = getRichValue;
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link';
export type MarkdownTag = {
match: string,
entityName: 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl';
};
export const markdownTags: {[type in MarkdownType]: MarkdownTag} = {
bold: {
match: '[style*="font-weight"], b',
entityName: 'messageEntityBold'
},
underline: {
match: '[style*="underline"], u',
entityName: 'messageEntityUnderline'
},
italic: {
match: '[style*="italic"], i',
entityName: 'messageEntityItalic'
},
monospace: {
match: '[style*="monospace"], [face="monospace"]',
entityName: 'messageEntityPre'
},
strikethrough: {
match: '[style*="line-through"], strike',
entityName: 'messageEntityStrike'
},
link: {
match: 'A',
entityName: 'messageEntityTextUrl'
}
};
function getRichElementValue(node: HTMLElement, lines: string[], line: string[], selNode?: Node, selOffset?: number, entities?: MessageEntity[], offset = {offset: 0}) {
if(node.nodeType === 3) { // TEXT
if(selNode === node) {
const value = node.nodeValue;
line.push(value.substr(0, selOffset) + '\x01' + value.substr(selOffset));
} else {
const nodeValue = node.nodeValue;
line.push(nodeValue);
if(entities && nodeValue.trim()) {
if(node.parentNode) {
const parentElement = node.parentElement;
for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType];
const closest = parentElement.closest(tag.match + ', [contenteditable]');
if(closest && closest.getAttribute('contenteditable') === null) {
if(tag.entityName === 'messageEntityTextUrl') {
entities.push({
_: tag.entityName as any,
url: (parentElement as HTMLAnchorElement).href,
offset: offset.offset,
length: nodeValue.length
});
} else {
entities.push({
_: tag.entityName as any,
offset: offset.offset,
length: nodeValue.length
});
}
}
}
}
}
offset.offset += nodeValue.length;
}
return;
}
if(node.nodeType !== 1) { // NON-ELEMENT
return;
}
const isSelected = (selNode === node);
const isBlock = node.tagName === 'DIV' || node.tagName === 'P';
if(isBlock && line.length || node.tagName === 'BR') {
lines.push(line.join(''));
line.splice(0, line.length);
} else if(node.tagName === 'IMG') {
const alt = (node as HTMLImageElement).alt;
if(alt) {
line.push(alt);
offset.offset += alt.length;
}
}
if(isSelected && !selOffset) {
line.push('\x01');
}
let curChild = node.firstChild as HTMLElement;
while(curChild) {
getRichElementValue(curChild, lines, line, selNode, selOffset, entities, offset);
curChild = curChild.nextSibling as any;
}
if(isSelected && selOffset) {
line.push('\x01');
}
if(isBlock && line.length) {
lines.push(line.join(''));
line.splice(0, line.length);
}
}

52
src/helpers/dom/getRichValueWithCaret.ts

@ -0,0 +1,52 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { MessageEntity } from "../../layer";
import RichTextProcessor from "../../lib/richtextprocessor";
import getRichElementValue from "./getRichElementValue";
export default function getRichValueWithCaret(field: HTMLElement, withEntities = true) {
const lines: string[] = [];
const line: string[] = [];
const sel = window.getSelection();
var selNode
var selOffset
if(sel && sel.rangeCount) {
const range = sel.getRangeAt(0);
if(range.startContainer &&
range.startContainer == range.endContainer &&
range.startOffset == range.endOffset) {
selNode = range.startContainer;
selOffset = range.startOffset;
}
}
const entities: MessageEntity[] = withEntities ? [] : undefined;
getRichElementValue(field, lines, line, selNode, selOffset, entities);
if(line.length) {
lines.push(line.join(''));
}
let value = lines.join('\n');
const caretPos = value.indexOf('\x01');
if(caretPos != -1) {
value = value.substr(0, caretPos) + value.substr(caretPos + 1);
}
value = value.replace(/\u00A0/g, ' ');
if(entities) {
RichTextProcessor.combineSameEntities(entities);
}
return {value, entities, caretPos};
}

2
src/helpers/dom/isInputEmpty.ts

@ -11,7 +11,7 @@ export default function isInputEmpty(element: HTMLElement) {
/* const value = element.innerText; /* const value = element.innerText;
return !value.trim() && !serializeNodes(Array.from(element.childNodes)).trim(); */ return !value.trim() && !serializeNodes(Array.from(element.childNodes)).trim(); */
return !getRichValue(element).trim(); return !getRichValue(element, false).value.trim();
} else { } else {
return !(element as HTMLInputElement).value.trim(); return !(element as HTMLInputElement).value.trim();
} }

2
src/lib/appManagers/appPollsManager.ts

@ -148,6 +148,8 @@ export class AppPollsManager {
} }
solution = RichTextProcessor.parseMarkdown(solution, solutionEntities); solution = RichTextProcessor.parseMarkdown(solution, solutionEntities);
} else {
solution = undefined; // can be string here
} }
return { return {

28
src/scss/partials/_autocompleteHelper.scss

@ -0,0 +1,28 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.autocomplete-helper {
--border-radius: #{$border-radius-medium};
position: absolute !important;
bottom: calc(100% + .625rem);
overflow: hidden;
padding: 0 !important;
border-radius: var(--border-radius) !important;
&:not(.is-visible) {
display: none;
}
@include animation-level(2) {
&.is-visible {
animation: fade-out-opacity .2s ease-in-out forwards;
&:not(.backwards) {
animation: fade-in-opacity .2s ease-in-out forwards;
}
}
}
}

37
src/scss/partials/_chatStickersHelper.scss

@ -5,47 +5,22 @@
*/ */
.stickers-helper { .stickers-helper {
position: absolute !important;
bottom: calc(100% + 10px);
overflow: hidden;
padding: 0 !important;
border-radius: 10px !important;
> .scrollable { > .scrollable {
position: relative; position: relative;
max-height: 220px; max-height: 13.75rem;
min-height: var(--esg-sticker-size); min-height: var(--esg-sticker-size);
padding: 7px; padding: .4375rem;
} }
&-stickers { &-stickers {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
border-radius: var(--border-radius);
} }
&-sticker { .super-sticker:not(.active) {
position: relative; @include hover() {
width: var(--esg-sticker-size); background: none;
height: var(--esg-sticker-size);
margin: 5px;
img {
width: 100%;
height: 100%;
}
}
&:not(.is-visible) {
display: none;
}
@include animation-level(2) {
&.is-visible {
animation: fade-out-opacity .2s ease-in-out forwards;
&:not(.backwards) {
animation: fade-in-opacity .2s ease-in-out forwards;
}
} }
} }
} }

8
src/scss/style.scss

@ -225,6 +225,7 @@ html.night {
@import "partials/input"; @import "partials/input";
@import "partials/button"; @import "partials/button";
@import "partials/animatedIcon"; @import "partials/animatedIcon";
@import "partials/autocompleteHelper";
@import "partials/badge"; @import "partials/badge";
@import "partials/checkbox"; @import "partials/checkbox";
@import "partials/chatlist"; @import "partials/chatlist";
@ -1205,3 +1206,10 @@ middle-ellipsis-element {
.verified-background { .verified-background {
fill: #33a8e5; fill: #33a8e5;
} }
.navigable-list {
.active {
background-color: var(--light-secondary-text-color);
border-radius: inherit;
}
}

Loading…
Cancel
Save