/* * 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 { 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; 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 = 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 selectionTop = 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) { 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; } if(document.activeElement !== this.appImManager.chat.input.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 { this.setMouseUpEvent(); } }); } }