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.
 
 
 
 
 

285 lines
8.9 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import rootScope from "../../lib/rootScope";
import ripple from "../ripple";
import animationIntersector from "../animationIntersector";
import appNavigationController, { NavigationItem } from "../appNavigationController";
import { i18n, LangPackKey } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName";
import blurActiveElement from "../../helpers/dom/blurActiveElement";
import ListenerSetter from "../../helpers/listenerSetter";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import isSendShortcutPressed from "../../helpers/dom/isSendShortcutPressed";
import cancelEvent from "../../helpers/dom/cancelEvent";
import EventListenerBase, { EventListenerListeners } from "../../helpers/eventListenerBase";
import { addFullScreenListener, getFullScreenElement } from "../../helpers/dom/fullScreen";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { AppManagers } from "../../lib/appManagers/managers";
import overlayCounter from "../../helpers/overlayCounter";
export type PopupButton = {
text?: string,
callback?: () => void,
langKey?: LangPackKey,
langArgs?: any[],
isDanger?: true,
isCancel?: true,
element?: HTMLButtonElement
};
export type PopupOptions = Partial<{
closable: boolean,
overlayClosable: boolean,
withConfirm: LangPackKey | boolean,
body: boolean,
confirmShortcutIsSendShortcut: boolean,
withoutOverlay: boolean
}>;
export interface PopupElementConstructable<T extends PopupElement = any> {
new(...args: any[]): T;
}
const DEFAULT_APPEND_TO = document.body;
let appendPopupTo = DEFAULT_APPEND_TO;
const onFullScreenChange = () => {
appendPopupTo = getFullScreenElement() || DEFAULT_APPEND_TO;
PopupElement.reAppend();
};
addFullScreenListener(DEFAULT_APPEND_TO, onFullScreenChange);
type PopupListeners = {
close: () => void,
closeAfterTimeout: () => void
};
export default class PopupElement<T extends EventListenerListeners = {}> extends EventListenerBase<PopupListeners & T> {
private static POPUPS: PopupElement<any>[] = [];
public static MANAGERS: AppManagers;
protected element = document.createElement('div');
protected container = document.createElement('div');
protected header = document.createElement('div');
protected title = document.createElement('div');
protected btnClose: HTMLElement;
protected btnConfirm: HTMLButtonElement;
protected body: HTMLElement;
protected buttonsEl: HTMLElement;
protected onEscape: () => boolean = () => true;
protected navigationItem: NavigationItem;
protected listenerSetter: ListenerSetter;
protected confirmShortcutIsSendShortcut: boolean;
protected btnConfirmOnEnter: HTMLButtonElement;
protected withoutOverlay: boolean;
protected managers: AppManagers;
constructor(className: string, protected buttons?: Array<PopupButton>, options: PopupOptions = {}) {
super(false);
this.element.classList.add('popup');
this.element.className = 'popup' + (className ? ' ' + className : '');
this.container.classList.add('popup-container', 'z-depth-1');
this.header.classList.add('popup-header');
this.title.classList.add('popup-title');
this.header.append(this.title);
this.listenerSetter = new ListenerSetter();
this.managers = PopupElement.MANAGERS;
this.confirmShortcutIsSendShortcut = options.confirmShortcutIsSendShortcut;
if(options.closable) {
this.btnClose = document.createElement('span');
this.btnClose.classList.add('btn-icon', 'popup-close', 'tgico-close');
//ripple(this.closeBtn);
this.header.prepend(this.btnClose);
attachClickEvent(this.btnClose, this.hide, {listenerSetter: this.listenerSetter, once: true});
}
this.withoutOverlay = options.withoutOverlay;
if(this.withoutOverlay) {
this.element.classList.add('no-overlay');
}
if(options.overlayClosable) {
attachClickEvent(this.element, (e: MouseEvent) => {
if(!findUpClassName(e.target, 'popup-container')) {
this.hide();
}
}, {listenerSetter: this.listenerSetter});
}
if(options.withConfirm) {
this.btnConfirm = document.createElement('button');
this.btnConfirm.classList.add('btn-primary', 'btn-color-primary');
if(options.withConfirm !== true) {
this.btnConfirm.append(i18n(options.withConfirm));
}
this.header.append(this.btnConfirm);
ripple(this.btnConfirm);
}
this.container.append(this.header);
if(options.body) {
this.body = document.createElement('div');
this.body.classList.add('popup-body');
this.container.append(this.body);
}
let btnConfirmOnEnter = this.btnConfirm;
if(buttons?.length) {
const buttonsDiv = this.buttonsEl = document.createElement('div');
buttonsDiv.classList.add('popup-buttons');
if(buttons.length === 2) {
buttonsDiv.classList.add('popup-buttons-row');
}
const buttonsElements = buttons.map((b) => {
const button = document.createElement('button');
button.className = 'btn' + (b.isDanger ? ' danger' : ' primary');
ripple(button);
if(b.text) {
button.innerHTML = b.text;
} else {
button.append(i18n(b.langKey, b.langArgs));
}
attachClickEvent(button, () => {
b.callback && b.callback();
this.destroy();
}, {listenerSetter: this.listenerSetter, once: true});
return b.element = button;
});
if(!btnConfirmOnEnter && buttons.length === 2) {
const button = buttons.find((button) => !button.isCancel);
if(button) {
btnConfirmOnEnter = button.element;
}
}
buttonsDiv.append(...buttonsElements);
this.container.append(buttonsDiv);
}
this.btnConfirmOnEnter = btnConfirmOnEnter;
this.element.append(this.container);
PopupElement.POPUPS.push(this);
}
public show() {
this.navigationItem = {
type: 'popup',
onPop: () => this.destroy(),
onEscape: this.onEscape
};
appNavigationController.pushItem(this.navigationItem);
blurActiveElement(); // * hide mobile keyboard
appendPopupTo.append(this.element);
void this.element.offsetWidth; // reflow
this.element.classList.add('active');
if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = true;
animationIntersector.checkAnimations(true);
}
// cannot add event instantly because keydown propagation will fire it
if(this.btnConfirmOnEnter) {
setTimeout(() => {
this.listenerSetter.add(document.body)('keydown', (e) => {
if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') {
simulateClickEvent(this.btnConfirmOnEnter);
cancelEvent(e);
}
});
}, 0);
}
}
public hide = () => {
appNavigationController.backByItem(this.navigationItem);
};
protected destroy() {
this.dispatchEvent<PopupListeners>('close');
this.element.classList.add('hiding');
this.element.classList.remove('active');
this.listenerSetter.removeAll();
if(!this.withoutOverlay) {
overlayCounter.isOverlayActive = false;
}
appNavigationController.removeItem(this.navigationItem);
this.navigationItem = undefined;
indexOfAndSplice(PopupElement.POPUPS, this);
// ! calm
onFullScreenChange();
setTimeout(() => {
this.element.remove();
this.dispatchEvent<PopupListeners>('closeAfterTimeout');
this.cleanup();
if(!this.withoutOverlay) {
animationIntersector.checkAnimations(false);
}
}, 150);
}
public static reAppend() {
this.POPUPS.forEach((popup) => {
const {element, container} = popup;
const parentElement = element.parentElement;
if(parentElement && parentElement !== appendPopupTo && appendPopupTo !== container) {
appendPopupTo.append(element);
}
});
}
public static getPopups<T extends PopupElement>(popupConstructor: PopupElementConstructable<T>) {
return this.POPUPS.filter((element) => element instanceof popupConstructor) as T[];
}
public static createPopup<T extends PopupElement, A extends Array<any>>(ctor: {new(...args: A): T}, ...args: A) {
const popup = new ctor(...args);
return popup;
}
}
export const addCancelButton = (buttons: PopupButton[]) => {
const button = buttons.find((b) => b.isCancel);
if(!button) {
buttons.push({
langKey: 'Cancel',
isCancel: true
});
}
return buttons;
};