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
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(); |
|
} |
|
}); |
|
} |
|
}
|
|
|