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.
354 lines
11 KiB
354 lines
11 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import { MOUNT_CLASS_TO } from "../config/debug"; |
|
import { cancelEvent } from "../helpers/dom/cancelEvent"; |
|
import { CLICK_EVENT_NAME } from "../helpers/dom/clickEvent"; |
|
import ListenerSetter from "../helpers/listenerSetter"; |
|
import mediaSizes from "../helpers/mediaSizes"; |
|
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; |
|
import { IS_APPLE, IS_MOBILE_SAFARI } from "../environment/userAgent"; |
|
import rootScope from "../lib/rootScope"; |
|
import appNavigationController from "./appNavigationController"; |
|
|
|
export function putPreloader(elem: Element, returnDiv = false): HTMLElement { |
|
const html = ` |
|
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50"> |
|
<circle class="preloader-path" cx="50" cy="50" r="20" fill="none" stroke-miterlimit="10"/> |
|
</svg>`; |
|
|
|
if(returnDiv) { |
|
const div = document.createElement('div'); |
|
div.classList.add('preloader'); |
|
div.innerHTML = html; |
|
|
|
if(elem) { |
|
elem.appendChild(div); |
|
} |
|
|
|
return div; |
|
} |
|
|
|
elem.insertAdjacentHTML('beforeend', html); |
|
return elem.lastElementChild as HTMLElement; |
|
} |
|
|
|
MOUNT_CLASS_TO.putPreloader = putPreloader; |
|
|
|
export function setButtonLoader(elem: HTMLButtonElement, icon = 'check') { |
|
elem.classList.remove('tgico-' + icon); |
|
elem.disabled = true; |
|
putPreloader(elem); |
|
|
|
return () => { |
|
elem.innerHTML = ''; |
|
elem.classList.add('tgico-' + icon); |
|
elem.removeAttribute('disabled'); |
|
}; |
|
} |
|
|
|
/* export function parseMenuButtonsTo(to: {[name: string]: HTMLElement}, elements: HTMLCollection | NodeListOf<HTMLElement>) { |
|
Array.from(elements).forEach(el => { |
|
const match = el.className.match(/(?:^|\s)menu-(.+?)(?:$|\s)/); |
|
if(!match) return; |
|
to[match[1]] = el as HTMLElement; |
|
}); |
|
} */ |
|
|
|
let onMouseMove = (e: MouseEvent) => { |
|
let rect = openedMenu.getBoundingClientRect(); |
|
let {clientX, clientY} = e; |
|
|
|
let diffX = clientX >= rect.right ? clientX - rect.right : rect.left - clientX; |
|
let diffY = clientY >= rect.bottom ? clientY - rect.bottom : rect.top - clientY; |
|
|
|
if(diffX >= 100 || diffY >= 100) { |
|
closeBtnMenu(); |
|
//openedMenu.parentElement.click(); |
|
} |
|
//console.log('mousemove', diffX, diffY); |
|
}; |
|
|
|
const onClick = (e: MouseEvent | TouchEvent) => { |
|
//cancelEvent(e); |
|
closeBtnMenu(); |
|
}; |
|
|
|
// ! no need in this due to the same handler in appNavigationController |
|
/* const onKeyDown = (e: KeyboardEvent) => { |
|
if(e.key === 'Escape') { |
|
closeBtnMenu(); |
|
cancelEvent(e); |
|
} |
|
}; */ |
|
|
|
export const closeBtnMenu = () => { |
|
if(openedMenu) { |
|
openedMenu.classList.remove('active'); |
|
openedMenu.parentElement.classList.remove('menu-open'); |
|
//openedMenu.previousElementSibling.remove(); // remove overlay |
|
if(menuOverlay) menuOverlay.remove(); |
|
openedMenu = null; |
|
|
|
rootScope.dispatchEvent('context_menu_toggle', false); |
|
} |
|
|
|
if(openedMenuOnClose) { |
|
openedMenuOnClose(); |
|
openedMenuOnClose = null; |
|
} |
|
|
|
if(!IS_TOUCH_SUPPORTED) { |
|
window.removeEventListener('mousemove', onMouseMove); |
|
//window.removeEventListener('keydown', onKeyDown, {capture: true}); |
|
window.removeEventListener('contextmenu', onClick); |
|
} |
|
|
|
document.removeEventListener(CLICK_EVENT_NAME, onClick); |
|
|
|
if(!IS_MOBILE_SAFARI) { |
|
appNavigationController.removeByType('menu'); |
|
} |
|
}; |
|
|
|
window.addEventListener('resize', () => { |
|
if(openedMenu) { |
|
closeBtnMenu(); |
|
} |
|
|
|
/* if(openedMenu && (openedMenu.style.top || openedMenu.style.left)) { |
|
const rect = openedMenu.getBoundingClientRect(); |
|
const {innerWidth, innerHeight} = window; |
|
|
|
console.log(innerWidth, innerHeight, rect); |
|
} */ |
|
}); |
|
|
|
let openedMenu: HTMLElement = null, openedMenuOnClose: () => void = null, menuOverlay: HTMLElement = null; |
|
export function openBtnMenu(menuElement: HTMLElement, onClose?: () => void) { |
|
closeBtnMenu(); |
|
|
|
if(!IS_MOBILE_SAFARI) { |
|
appNavigationController.pushItem({ |
|
type: 'menu', |
|
onPop: (canAnimate) => { |
|
closeBtnMenu(); |
|
} |
|
}); |
|
} |
|
|
|
openedMenu = menuElement; |
|
openedMenu.classList.add('active'); |
|
openedMenu.parentElement.classList.add('menu-open'); |
|
|
|
if(!menuOverlay) { |
|
menuOverlay = document.createElement('div'); |
|
menuOverlay.classList.add('btn-menu-overlay'); |
|
|
|
// ! because this event must be canceled, and can't cancel on menu click (below) |
|
menuOverlay.addEventListener(CLICK_EVENT_NAME, (e) => { |
|
cancelEvent(e); |
|
onClick(e); |
|
}); |
|
} |
|
|
|
openedMenu.parentElement.insertBefore(menuOverlay, openedMenu); |
|
|
|
//document.body.classList.add('disable-hover'); |
|
|
|
openedMenuOnClose = onClose; |
|
|
|
if(!IS_TOUCH_SUPPORTED) { |
|
window.addEventListener('mousemove', onMouseMove); |
|
//window.addEventListener('keydown', onKeyDown, {capture: true}); |
|
window.addEventListener('contextmenu', onClick, {once: true}); |
|
} |
|
|
|
/* // ! because this event must be canceled, and can't cancel on menu click (below) |
|
overlay.addEventListener(CLICK_EVENT_NAME, (e) => { |
|
cancelEvent(e); |
|
onClick(e); |
|
}); */ |
|
|
|
// ! safari iOS doesn't handle window click event on overlay, idk why |
|
document.addEventListener(CLICK_EVENT_NAME, onClick); |
|
|
|
rootScope.dispatchEvent('context_menu_toggle', true); |
|
} |
|
|
|
const PADDING_TOP = 8; |
|
const PADDING_LEFT = 8; |
|
export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLElement, side?: 'left' | 'right' | 'center') { |
|
//let {clientX, clientY} = e; |
|
|
|
// * side mean the OPEN side |
|
|
|
let {scrollWidth: menuWidth, scrollHeight: menuHeight} = elem; |
|
//let {innerWidth: windowWidth, innerHeight: windowHeight} = window; |
|
const rect = document.body.getBoundingClientRect(); |
|
const windowWidth = rect.width; |
|
const windowHeight = rect.height; |
|
|
|
side = mediaSizes.isMobile ? 'right' : 'left'; |
|
let verticalSide: 'top' /* | 'bottom' */ | 'center' = 'top'; |
|
|
|
const getSides = () => { |
|
return { |
|
x: { |
|
left: pageX, |
|
right: pageX - menuWidth |
|
}, |
|
intermediateX: side === 'right' ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, |
|
//intermediateX: clientX < windowWidth / 2 ? PADDING_LEFT : windowWidth - menuWidth - PADDING_LEFT, |
|
y: { |
|
top: pageY, |
|
bottom: pageY - menuHeight |
|
}, |
|
//intermediateY: verticalSide === 'top' ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, |
|
intermediateY: pageY < windowHeight / 2 ? PADDING_TOP : windowHeight - menuHeight - PADDING_TOP, |
|
}; |
|
}; |
|
|
|
const sides = getSides(); |
|
|
|
const possibleSides = { |
|
x: { |
|
left: sides.x.left + menuWidth + PADDING_LEFT <= windowWidth, |
|
right: sides.x.right >= PADDING_LEFT |
|
}, |
|
y: { |
|
top: sides.y.top + menuHeight + PADDING_TOP <= windowHeight, |
|
bottom: sides.y.bottom - PADDING_TOP >= PADDING_TOP |
|
} |
|
}; |
|
|
|
/* if(side === undefined) { |
|
if((clientX + menuWidth + PADDING_LEFT) > windowWidth) { |
|
side = 'right'; |
|
} |
|
} */ |
|
|
|
{ |
|
/* const x = sides.x; |
|
|
|
const s = Object.keys(x) as (keyof typeof possibleSides.x)[]; |
|
if(side) { |
|
s.findAndSplice(s => s === side); |
|
s.unshift(side); |
|
} |
|
|
|
const possibleSide = s.find(s => possibleSides.x[s]); */ |
|
let left: number; |
|
/* if(possibleSide) { |
|
left = x[possibleSide]; |
|
side = possibleSide; |
|
} else { |
|
left = sides.intermediateX; |
|
side = undefined; |
|
} */ |
|
left = possibleSides.x[side] ? sides.x[side] : (side = 'center', sides.intermediateX); |
|
|
|
elem.style.left = left + 'px'; |
|
} |
|
|
|
/* if((clientY + menuHeight + PADDING_TOP) > windowHeight) { |
|
elem.style.top = clamp(clientY - menuHeight, PADDING_TOP, windowHeight - menuHeight - PADDING_TOP) + 'px'; |
|
// elem.style.top = (innerHeight - scrollHeight - PADDING_TOP) + 'px'; |
|
verticalSide = 'bottom'; |
|
} else { |
|
elem.style.top = Math.max(PADDING_TOP, clientY) + 'px'; |
|
verticalSide = 'top'; |
|
} */ |
|
|
|
{ |
|
let top: number; |
|
|
|
top = possibleSides.y[verticalSide] ? sides.y[verticalSide] : (verticalSide = 'center', sides.intermediateY); |
|
|
|
elem.style.top = top + 'px'; |
|
} |
|
|
|
elem.className = elem.className.replace(/(top|center|bottom)-(left|center|right)/g, ''); |
|
elem.classList.add( |
|
//(verticalSide === 'center' ? verticalSide : (verticalSide === 'bottom' ? 'top' : 'bottom')) + |
|
(verticalSide === 'center' ? verticalSide : 'bottom') + |
|
'-' + |
|
(side === 'center' ? side : (side === 'left' ? 'right' : 'left'))); |
|
} |
|
|
|
let _cancelContextMenuOpening = false, _cancelContextMenuOpeningTimeout = 0; |
|
export function cancelContextMenuOpening() { |
|
if(_cancelContextMenuOpeningTimeout) { |
|
clearTimeout(_cancelContextMenuOpeningTimeout); |
|
} |
|
|
|
_cancelContextMenuOpeningTimeout = window.setTimeout(() => { |
|
_cancelContextMenuOpeningTimeout = 0; |
|
_cancelContextMenuOpening = false; |
|
}, .4e3); |
|
|
|
_cancelContextMenuOpening = true; |
|
} |
|
|
|
export function attachContextMenuListener(element: HTMLElement, callback: (e: Touch | MouseEvent) => void, listenerSetter?: ListenerSetter) { |
|
const add = listenerSetter ? listenerSetter.add(element) : element.addEventListener.bind(element); |
|
const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, element) : element.removeEventListener.bind(element); |
|
|
|
if(IS_APPLE && IS_TOUCH_SUPPORTED) { |
|
let timeout: number; |
|
|
|
const options: EventListenerOptions = {capture: true}; |
|
|
|
const onCancel = () => { |
|
clearTimeout(timeout); |
|
// @ts-ignore |
|
remove('touchmove', onCancel, options); |
|
// @ts-ignore |
|
remove('touchend', onCancel, options); |
|
// @ts-ignore |
|
remove('touchcancel', onCancel, options); |
|
}; |
|
|
|
add('touchstart', (e: TouchEvent) => { |
|
if(e.touches.length > 1) { |
|
onCancel(); |
|
return; |
|
} |
|
|
|
add('touchmove', onCancel, options); |
|
add('touchend', onCancel, options); |
|
add('touchcancel', onCancel, options); |
|
|
|
timeout = window.setTimeout(() => { |
|
if(_cancelContextMenuOpening) { |
|
onCancel(); |
|
return; |
|
} |
|
|
|
callback(e.touches[0]); |
|
onCancel(); |
|
|
|
if(openedMenu) { |
|
element.addEventListener('touchend', cancelEvent, {once: true}); // * fix instant closing |
|
} |
|
}, .4e3); |
|
}); |
|
|
|
/* if(!isSafari) { |
|
add('contextmenu', (e: any) => { |
|
cancelEvent(e); |
|
}, {passive: false, capture: true}); |
|
} */ |
|
} else { |
|
add('contextmenu', IS_TOUCH_SUPPORTED ? (e: any) => { |
|
callback(e); |
|
|
|
if(openedMenu) { |
|
element.addEventListener('touchend', cancelEvent, {once: true}); // * fix instant closing |
|
} |
|
} : callback); |
|
} |
|
};
|
|
|