Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

428 lines
14 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { AppImManager } from "../../lib/appManagers/appImManager";
import RichTextProcessor from "../../lib/richtextprocessor";
import ButtonIcon from "../buttonIcon";
import { clamp } from "../../helpers/number";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import { IS_APPLE, IS_MOBILE } from "../../environment/userAgent";
import appNavigationController from "../appNavigationController";
import { _i18n } from "../../lib/langPack";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import getSelectedNodes from "../../helpers/dom/getSelectedNodes";
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
import { MarkdownType, markdownTags } from "../../helpers/dom/getRichElementValue";
import getVisibleRect from "../../helpers/dom/getVisibleRect";
//import { logger } from "../../lib/logger";
export default class MarkupTooltip {
public container: HTMLElement;
private wrapper: HTMLElement;
private buttons: {[type in MarkdownType]: HTMLElement} = {} as any;
private linkBackButton: HTMLElement;
private linkApplyButton: HTMLButtonElement;
private hideTimeout: number;
private addedListener = false;
private waitingForMouseUp = false;
private linkInput: HTMLInputElement;
private savedRange: Range;
private mouseUpCounter: number = 0;
//private log: ReturnType<typeof logger>;
constructor(private appImManager: AppImManager) {
//this.log = logger('MARKUP');
}
private init() {
this.container = document.createElement('div');
this.container.classList.add('markup-tooltip', 'z-depth-1', 'hide');
this.wrapper = document.createElement('div');
this.wrapper.classList.add('markup-tooltip-wrapper');
const tools1 = document.createElement('div');
const tools2 = document.createElement('div');
tools1.classList.add('markup-tooltip-tools');
tools2.classList.add('markup-tooltip-tools');
const arr = ['bold', 'italic', 'underline', 'strikethrough', 'monospace', 'spoiler', 'link'] as (keyof MarkupTooltip['buttons'])[];
arr.forEach(c => {
const button = ButtonIcon(c, {noRipple: true});
tools1.append(this.buttons[c] = button);
if(c !== 'link') {
button.addEventListener('mousedown', (e) => {
cancelEvent(e);
this.appImManager.chat.input.applyMarkdown(c);
this.cancelClosening();
/* this.mouseUpCounter = 0;
this.setMouseUpEvent(); */
//this.hide();
});
} else {
attachClickEvent(button, (e) => {
cancelEvent(e);
this.showLinkEditor();
this.cancelClosening();
});
}
});
this.linkBackButton = ButtonIcon('left', {noRipple: true});
this.linkInput = document.createElement('input');
_i18n(this.linkInput, 'MarkupTooltip.LinkPlaceholder', undefined, 'placeholder');
this.linkInput.classList.add('input-clear');
this.linkInput.addEventListener('keydown', (e) => {
const valid = !this.linkInput.value.length || !!RichTextProcessor.matchUrl(this.linkInput.value);///^(http)|(https):\/\//i.test(this.linkInput.value);
if(e.key === 'Enter') {
if(!valid) {
if(this.linkInput.classList.contains('error')) {
this.linkInput.classList.remove('error');
void this.linkInput.offsetLeft; // reflow
}
this.linkInput.classList.add('error');
} else {
this.applyLink(e);
}
}
});
this.linkInput.addEventListener('input', (e) => {
const valid = this.isLinkValid();
this.linkInput.classList.toggle('is-valid', valid);
this.linkInput.classList.remove('error');
});
this.linkBackButton.addEventListener('mousedown', (e) => {
//this.log('linkBackButton click');
cancelEvent(e);
this.container.classList.remove('is-link');
//input.value = '';
this.resetSelection();
this.setTooltipPosition();
this.cancelClosening();
});
this.linkApplyButton = ButtonIcon('check markup-tooltip-link-apply', {noRipple: true});
this.linkApplyButton.addEventListener('mousedown', (e) => {
//this.log('linkApplyButton click');
this.applyLink(e);
});
const applyDiv = document.createElement('div');
applyDiv.classList.add('markup-tooltip-link-apply-container');
const delimiter1 = document.createElement('span');
const delimiter2 = document.createElement('span');
const delimiter3 = document.createElement('span');
delimiter1.classList.add('markup-tooltip-delimiter');
delimiter2.classList.add('markup-tooltip-delimiter');
delimiter3.classList.add('markup-tooltip-delimiter');
tools1.insertBefore(delimiter1, this.buttons.link);
applyDiv.append(delimiter3, this.linkApplyButton);
tools2.append(this.linkBackButton, delimiter2, this.linkInput, applyDiv);
//tools1.insertBefore(delimiter2, this.buttons.link.nextSibling);
this.wrapper.append(tools1, tools2);
this.container.append(this.wrapper);
document.body.append(this.container);
window.addEventListener('resize', () => {
this.hide();
});
}
public showLinkEditor() {
if(!this.container || !this.container.classList.contains('is-visible')) { // * if not inited yet (Ctrl+A + Ctrl+K)
this.show();
}
const button = this.buttons.link;
this.container.classList.add('is-link');
const selection = document.getSelection();
this.savedRange = selection.getRangeAt(0);
if(button.classList.contains('active')) {
const startContainer = this.savedRange.startContainer;
const anchor = startContainer.parentElement as HTMLAnchorElement;
this.linkInput.value = anchor.href;
} else {
this.linkInput.value = '';
}
this.setTooltipPosition(true);
setTimeout(() => {
this.linkInput.focus(); // !!! instant focus will break animation
}, 200);
this.linkInput.classList.toggle('is-valid', this.isLinkValid());
}
private applyLink(e: Event) {
cancelEvent(e);
this.resetSelection();
let url = this.linkInput.value;
if(url && !RichTextProcessor.matchUrlProtocol(url)) {
url = 'https://' + url;
}
this.appImManager.chat.input.applyMarkdown('link', url);
setTimeout(() => {
this.hide();
}, 0);
}
private isLinkValid() {
return !this.linkInput.value.length || !!RichTextProcessor.matchUrl(this.linkInput.value);
}
private resetSelection(range: Range = this.savedRange) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
this.appImManager.chat.input.messageInput.focus();
}
public hide() {
//return;
if(this.init) return;
this.container.classList.remove('is-visible');
//document.removeEventListener('mouseup', this.onMouseUp);
document.removeEventListener('mouseup', this.onMouseUpSingle);
this.waitingForMouseUp = false;
appNavigationController.removeByType('markup');
if(this.hideTimeout) clearTimeout(this.hideTimeout);
this.hideTimeout = window.setTimeout(() => {
this.hideTimeout = undefined;
this.container.classList.add('hide');
this.container.classList.remove('is-link');
}, 200);
}
public getActiveMarkupButton() {
const nodes = getSelectedNodes();
const parents = [...new Set(nodes.map(node => node.parentNode))];
//if(parents.length > 1 && parents) return [];
const currentMarkups: Set<HTMLElement> = new Set();
(parents as HTMLElement[]).forEach(node => {
for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType];
const closest = node.closest(tag.match + ', [contenteditable]');
if(closest !== this.appImManager.chat.input.messageInput) {
currentMarkups.add(this.buttons[type as MarkdownType]);
}
}
});
return [...currentMarkups];
}
public setActiveMarkupButton() {
const activeButtons = this.getActiveMarkupButton();
for(const i in this.buttons) {
// @ts-ignore
const button = this.buttons[i];
button.classList.toggle('active', activeButtons.includes(button));
}
}
private setTooltipPosition(isLinkToggle = false) {
const selection = document.getSelection();
const range = selection.getRangeAt(0);
const bodyRect = document.body.getBoundingClientRect();
const selectionRect = range.getBoundingClientRect();
const inputRect = this.appImManager.chat.input.rowsWrapper.getBoundingClientRect();
this.container.style.maxWidth = inputRect.width + 'px';
const visibleRect = getVisibleRect(undefined, this.appImManager.chat.input.messageInput, false, selectionRect);
const selectionTop = visibleRect.rect.top/* selectionRect.top */ + (bodyRect.top * -1);
const currentTools = this.container.classList.contains('is-link') ? this.wrapper.lastElementChild : this.wrapper.firstElementChild;
const sizesRect = currentTools.getBoundingClientRect();
const top = selectionTop - sizesRect.height - 8;
const minX = inputRect.left;
const maxX = (inputRect.left + inputRect.width) - Math.min(inputRect.width, sizesRect.width);
let left: number;
if(isLinkToggle) {
const containerRect = this.container.getBoundingClientRect();
left = clamp(containerRect.left, minX, maxX);
} else {
const x = selectionRect.left + (selectionRect.width - sizesRect.width) / 2;
left = clamp(x, minX, maxX);
}
/* const isClamped = x !== minX && x !== maxX && (left === minX || left === maxX || this.container.getBoundingClientRect().left >= maxX);
if(isLinkToggle && this.container.classList.contains('is-link') && !isClamped) return; */
this.container.style.transform = `translate3d(${left}px, ${top}px, 0)`;
}
public show() {
if(this.init) {
this.init();
this.init = null;
}
if(isSelectionEmpty()) {
this.hide();
return;
}
if(this.hideTimeout !== undefined) {
clearTimeout(this.hideTimeout);
}
if(this.container.classList.contains('is-visible')) {
return;
}
this.setActiveMarkupButton();
this.container.classList.remove('is-link');
const isFirstShow = this.container.classList.contains('hide');
if(isFirstShow) {
this.container.classList.remove('hide');
this.container.classList.add('no-transition');
}
this.setTooltipPosition();
if(isFirstShow) {
void this.container.offsetLeft; // reflow
this.container.classList.remove('no-transition');
}
this.container.classList.add('is-visible');
if(!IS_MOBILE) {
appNavigationController.pushItem({
type: 'markup',
onPop: () => {
this.hide();
}
});
}
//this.log('selection', selectionRect, activeButton);
}
/* private onMouseUp = (e: Event) => {
this.log('onMouseUp');
if(findUpClassName(e.target, 'markup-tooltip')) return;
this.hide();
//document.removeEventListener('mouseup', this.onMouseUp);
}; */
private onMouseUpSingle = (e?: Event) => {
//this.log('onMouseUpSingle');
this.waitingForMouseUp = false;
if(IS_TOUCH_SUPPORTED) {
e && cancelEvent(e);
if(this.mouseUpCounter++ === 0) {
this.resetSelection(this.savedRange);
} else {
this.hide();
return;
}
}
this.show();
/* !isTouchSupported && document.addEventListener('mouseup', this.onMouseUp); */
};
public setMouseUpEvent() {
if(this.waitingForMouseUp) return;
this.waitingForMouseUp = true;
//this.log('setMouseUpEvent');
document.addEventListener('mouseup', this.onMouseUpSingle, {once: true});
}
public cancelClosening() {
if(IS_TOUCH_SUPPORTED && !IS_APPLE) {
document.removeEventListener('mouseup', this.onMouseUpSingle);
document.addEventListener('mouseup', (e) => {
cancelEvent(e);
this.mouseUpCounter = 1;
this.waitingForMouseUp = false;
this.setMouseUpEvent();
}, {once: true});
}
}
public handleSelection() {
if(this.addedListener) return;
this.addedListener = true;
document.addEventListener('selectionchange', (e) => {
//this.log('selectionchange');
if(document.activeElement === this.linkInput) {
return;
}
const messageInput = this.appImManager.chat.input.messageInput;
if(document.activeElement !== messageInput) {
this.hide();
return;
}
const selection = document.getSelection();
if(isSelectionEmpty(selection)) {
this.hide();
return;
}
if(IS_TOUCH_SUPPORTED) {
if(IS_APPLE) {
this.show();
this.setTooltipPosition(); // * because can skip this in .show();
} else {
if(this.mouseUpCounter === 2) {
this.mouseUpCounter = 0;
return;
}
this.savedRange = selection.getRangeAt(0);
this.setMouseUpEvent();
/* document.addEventListener('touchend', (e) => {
cancelEvent(e);
this.resetSelection(range);
this.show();
}, {once: true, passive: false}); */
}
} else if(this.container && this.container.classList.contains('is-visible')) {
this.setTooltipPosition();
} else if(messageInput.matches(':active')) {
this.setMouseUpEvent();
} else {
this.show();
}
});
}
}