@ -9,30 +9,6 @@
@@ -9,30 +9,6 @@
* https : //github.com/zhukov/webogram/blob/master/LICENSE
* /
import { MessageEntity } from "../layer" ;
import RichTextProcessor from "../lib/richtextprocessor" ;
import ListenerSetter from "./listenerSetter" ;
import { isTouchSupported } from "./touchSupport" ;
import { isApple , isMobile , isMobileSafari } from "./userAgent" ;
import rootScope from "../lib/rootScope" ;
import { MOUNT_CLASS_TO } from "../config/debug" ;
import { doubleRaf } from "./schedulers" ;
/ * e x p o r t f u n c t i o n i s I n D O M ( e l e m e n t : E l e m e n t , p a r e n t N o d e ? : H T M L E l e m e n t ) : b o o l e a n {
if ( ! element ) {
return false ;
}
parentNode = parentNode || document . body ;
if ( element === parentNode ) {
return true ;
}
return isInDOM ( element . parentNode as HTMLElement , parentNode ) ;
} * /
export function isInDOM ( element : Element ) : boolean {
return element ? . isConnected ;
}
/ * e x p o r t f u n c t i o n c h e c k D r a g E v e n t ( e : a n y ) {
if ( ! e || e . target && ( e . target . tagName === 'IMG' || e . target . tagName === 'A' ) ) return false
if ( e . dataTransfer && e . dataTransfer . types ) {
@ -48,46 +24,6 @@ export function isInDOM(element: Element): boolean {
@@ -48,46 +24,6 @@ export function isInDOM(element: Element): boolean {
return false ;
} * /
export function cancelEvent ( event : Event ) {
event = event || window . event ;
if ( event ) {
// @ts-ignore
event = event . originalEvent || event ;
try {
if ( event . stopPropagation ) event . stopPropagation ( ) ;
if ( event . preventDefault ) event . preventDefault ( ) ;
event . returnValue = false ;
event . cancelBubble = true ;
} catch ( err ) { }
}
return false ;
}
export function placeCaretAtEnd ( el : HTMLElement ) {
if ( isTouchSupported ) {
return ;
}
el . focus ( ) ;
if ( typeof window . getSelection !== "undefined" && typeof document . createRange !== "undefined" ) {
var range = document . createRange ( ) ;
range . selectNodeContents ( el ) ;
range . collapse ( false ) ;
var sel = window . getSelection ( ) ;
sel . removeAllRanges ( ) ;
sel . addRange ( range ) ;
// @ts-ignore
} else if ( typeof document . body . createTextRange !== "undefined" ) {
// @ts-ignore
var textRange = document . body . createTextRange ( ) ;
textRange . moveToElementText ( el ) ;
textRange . collapse ( false ) ;
textRange . select ( ) ;
}
}
/ * e x p o r t f u n c t i o n g e t F i e l d S e l e c t i o n ( f i e l d : a n y ) {
if ( field . selectionStart ) {
return field . selectionStart ;
@ -120,154 +56,6 @@ export function placeCaretAtEnd(el: HTMLElement) {
@@ -120,154 +56,6 @@ export function placeCaretAtEnd(el: HTMLElement) {
return len ;
} * /
export function getRichValue ( field : HTMLElement , entities? : MessageEntity [ ] ) {
if ( ! field ) {
return '' ;
}
const lines : string [ ] = [ ] ;
const line : string [ ] = [ ] ;
getRichElementValue ( field , lines , line , undefined , undefined , entities ) ;
if ( line . length ) {
lines . push ( line . join ( '' ) ) ;
}
let value = lines . join ( '\n' ) ;
value = value . replace ( /\u00A0/g , ' ' ) ;
if ( entities ) {
RichTextProcessor . combineSameEntities ( entities ) ;
}
//console.log('getRichValue:', value, entities);
return value ;
}
MOUNT_CLASS_TO . getRichValue = getRichValue ;
export type MarkdownType = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'monospace' | 'link' ;
export type MarkdownTag = {
match : string ,
entityName : 'messageEntityBold' | 'messageEntityUnderline' | 'messageEntityItalic' | 'messageEntityPre' | 'messageEntityStrike' | 'messageEntityTextUrl' ;
} ;
export const markdownTags : { [ type in MarkdownType ] : MarkdownTag } = {
bold : {
match : '[style*="font-weight"], b' ,
entityName : 'messageEntityBold'
} ,
underline : {
match : '[style*="underline"], u' ,
entityName : 'messageEntityUnderline'
} ,
italic : {
match : '[style*="italic"], i' ,
entityName : 'messageEntityItalic'
} ,
monospace : {
match : '[style*="monospace"], [face="monospace"]' ,
entityName : 'messageEntityPre'
} ,
strikethrough : {
match : '[style*="line-through"], strike' ,
entityName : 'messageEntityStrike'
} ,
link : {
match : 'A' ,
entityName : 'messageEntityTextUrl'
}
} ;
export function getRichElementValue ( node : HTMLElement , lines : string [ ] , line : string [ ] , selNode? : Node , selOffset? : number , entities? : MessageEntity [ ] , offset = { offset : 0 } ) {
if ( node . nodeType === 3 ) { // TEXT
if ( selNode === node ) {
const value = node . nodeValue ;
line . push ( value . substr ( 0 , selOffset ) + '\x01' + value . substr ( selOffset ) ) ;
} else {
const nodeValue = node . nodeValue ;
line . push ( nodeValue ) ;
if ( entities && nodeValue . trim ( ) ) {
if ( node . parentNode ) {
const parentElement = node . parentElement ;
for ( const type in markdownTags ) {
const tag = markdownTags [ type as MarkdownType ] ;
const closest = parentElement . closest ( tag . match + ', [contenteditable]' ) ;
if ( closest && closest . getAttribute ( 'contenteditable' ) === null ) {
if ( tag . entityName === 'messageEntityTextUrl' ) {
entities . push ( {
_ : tag.entityName as any ,
url : ( parentElement as HTMLAnchorElement ) . href ,
offset : offset.offset ,
length : nodeValue.length
} ) ;
} else {
entities . push ( {
_ : tag.entityName as any ,
offset : offset.offset ,
length : nodeValue.length
} ) ;
}
}
}
}
}
offset . offset += nodeValue . length ;
}
return ;
}
if ( node . nodeType !== 1 ) { // NON-ELEMENT
return ;
}
const isSelected = ( selNode === node ) ;
const isBlock = node . tagName === 'DIV' || node . tagName === 'P' ;
if ( isBlock && line . length || node . tagName === 'BR' ) {
lines . push ( line . join ( '' ) ) ;
line . splice ( 0 , line . length ) ;
} else if ( node . tagName === 'IMG' ) {
const alt = ( node as HTMLImageElement ) . alt ;
if ( alt ) {
line . push ( alt ) ;
offset . offset += alt . length ;
}
}
if ( isSelected && ! selOffset ) {
line . push ( '\x01' ) ;
}
let curChild = node . firstChild as HTMLElement ;
while ( curChild ) {
getRichElementValue ( curChild , lines , line , selNode , selOffset , entities , offset ) ;
curChild = curChild . nextSibling as any ;
}
if ( isSelected && selOffset ) {
line . push ( '\x01' ) ;
}
if ( isBlock && line . length ) {
lines . push ( line . join ( '' ) ) ;
line . splice ( 0 , line . length ) ;
}
}
export function isInputEmpty ( element : HTMLElement ) {
if ( element . hasAttribute ( 'contenteditable' ) || element . tagName !== 'INPUT' ) {
/ * c o n s t v a l u e = e l e m e n t . i n n e r T e x t ;
return ! value . trim ( ) && ! serializeNodes ( Array . from ( element . childNodes ) ) . trim ( ) ; * /
return ! getRichValue ( element ) . trim ( ) ;
} else {
return ! ( element as HTMLInputElement ) . value . trim ( ) ;
}
}
/ * e x p o r t f u n c t i o n s e r i a l i z e N o d e s ( n o d e s : N o d e [ ] ) : s t r i n g {
return nodes . reduce ( ( str , child : any ) = > {
//console.log('childNode', str, child, typeof(child), typeof(child) === 'string', child.innerText);
@ -293,242 +81,6 @@ export function isInputEmpty(element: HTMLElement) {
@@ -293,242 +81,6 @@ export function isInputEmpty(element: HTMLElement) {
}
} * /
// generate a path's arc data parameter
// http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
var arcParameter = function ( rx : number , ry : number , xAxisRotation : number , largeArcFlag : number , sweepFlag : number , x : number , y : number ) {
return [ rx , ',' , ry , ' ' ,
xAxisRotation , ' ' ,
largeArcFlag , ',' ,
sweepFlag , ' ' ,
x , ',' , y ] . join ( '' ) ;
} ;
export function generatePathData ( x : number , y : number , width : number , height : number , tl : number , tr : number , br : number , bl : number ) {
const data : string [ ] = [ ] ;
// start point in top-middle of the rectangle
data . push ( 'M' + ( x + width / 2 ) + ',' + y ) ;
// next we go to the right
data . push ( 'H' + ( x + width - tr ) ) ;
if ( tr > 0 ) {
// now we draw the arc in the top-right corner
data . push ( 'A' + arcParameter ( tr , tr , 0 , 0 , 1 , ( x + width ) , ( y + tr ) ) ) ;
}
// next we go down
data . push ( 'V' + ( y + height - br ) ) ;
if ( br > 0 ) {
// now we draw the arc in the lower-right corner
data . push ( 'A' + arcParameter ( br , br , 0 , 0 , 1 , ( x + width - br ) , ( y + height ) ) ) ;
}
// now we go to the left
data . push ( 'H' + ( x + bl ) ) ;
if ( bl > 0 ) {
// now we draw the arc in the lower-left corner
data . push ( 'A' + arcParameter ( bl , bl , 0 , 0 , 1 , ( x + 0 ) , ( y + height - bl ) ) ) ;
}
// next we go up
data . push ( 'V' + ( y + tl ) ) ;
if ( tl > 0 ) {
// now we draw the arc in the top-left corner
data . push ( 'A' + arcParameter ( tl , tl , 0 , 0 , 1 , ( x + tl ) , ( y + 0 ) ) ) ;
}
// and we close the path
data . push ( 'Z' ) ;
return data . join ( ' ' ) ;
} ;
MOUNT_CLASS_TO . generatePathData = generatePathData ;
export function whichChild ( elem : Node ) {
if ( ! elem . parentNode ) {
return - 1 ;
}
let i = 0 ;
// @ts-ignore
while ( ( elem = elem . previousElementSibling ) !== null ) ++ i ;
return i ;
} ;
export function fillPropertyValue ( str : string ) {
let splitted = str . split ( ' ' ) ;
if ( splitted . length !== 4 ) {
if ( ! splitted [ 0 ] ) splitted [ 0 ] = '0px' ;
for ( let i = splitted . length ; i < 4 ; ++ i ) {
splitted [ i ] = splitted [ i % 2 ] || splitted [ 0 ] || '0px' ;
}
}
return splitted ;
}
export function calcImageInBox ( imageW : number , imageH : number , boxW : number , boxH : number , noZoom = true ) {
if ( imageW < boxW && imageH < boxH && noZoom ) {
return { w : imageW , h : imageH } ;
}
let boxedImageW = boxW ;
let boxedImageH = boxH ;
if ( ( imageW / imageH ) > ( boxW / boxH ) ) {
boxedImageH = ( imageH * boxW / imageW ) | 0 ;
} else {
boxedImageW = ( imageW * boxH / imageH ) | 0 ;
if ( boxedImageW > boxW ) {
boxedImageH = ( boxedImageH * boxW / boxedImageW ) | 0 ;
boxedImageW = boxW ;
}
}
// if (Config.Navigator.retina) {
// imageW = Math.floor(imageW / 2)
// imageH = Math.floor(imageH / 2)
// }
if ( noZoom && boxedImageW >= imageW && boxedImageH >= imageH ) {
boxedImageW = imageW ;
boxedImageH = imageH ;
}
return { w : boxedImageW , h : boxedImageH } ;
}
MOUNT_CLASS_TO . calcImageInBox = calcImageInBox ;
export function positionElementByIndex ( element : HTMLElement , container : HTMLElement , pos : number , prevPos? : number ) {
if ( prevPos === undefined ) {
prevPos = element . parentElement === container ? whichChild ( element ) : - 1 ;
}
if ( prevPos === pos ) {
return false ;
} else if ( prevPos !== - 1 && prevPos < pos ) { // was higher
pos += 1 ;
}
if ( container . childElementCount > pos ) {
container . insertBefore ( element , container . children [ pos ] ) ;
} else {
container . append ( element ) ;
}
return true ;
}
export function cancelSelection() {
if ( window . getSelection ) {
if ( window . getSelection ( ) . empty ) { // Chrome
window . getSelection ( ) . empty ( ) ;
} else if ( window . getSelection ( ) . removeAllRanges ) { // Firefox
window . getSelection ( ) . removeAllRanges ( ) ;
}
// @ts-ignore
} else if ( document . selection ) { // IE?
// @ts-ignore
document . selection . empty ( ) ;
}
}
//(window as any).splitStringByLength = splitStringByLength;
export function getSelectedText ( ) : string {
if ( window . getSelection ) {
return window . getSelection ( ) . toString ( ) ;
// @ts-ignore
} else if ( document . selection ) {
// @ts-ignore
return document . selection . createRange ( ) . text ;
}
return '' ;
}
export function blurActiveElement() {
if ( document . activeElement && ( document . activeElement as HTMLInputElement ) . blur ) {
( document . activeElement as HTMLInputElement ) . blur ( ) ;
return true ;
}
return false ;
}
export const CLICK_EVENT_NAME : 'mousedown' | 'touchend' | 'click' = ( isTouchSupported ? 'mousedown' : 'click' ) as any ;
export type AttachClickOptions = AddEventListenerOptions & Partial < { listenerSetter : ListenerSetter , touchMouseDown : true } > ;
export const attachClickEvent = ( elem : HTMLElement , callback : ( e : TouchEvent | MouseEvent ) = > void , options : AttachClickOptions = { } ) = > {
const add = options . listenerSetter ? options . listenerSetter . add . bind ( options . listenerSetter , elem ) : elem . addEventListener . bind ( elem ) ;
const remove = options . listenerSetter ? options . listenerSetter . removeManual . bind ( options . listenerSetter , elem ) : elem . removeEventListener . bind ( elem ) ;
options . touchMouseDown = true ;
/ * i f ( o p t i o n s . t o u c h M o u s e D o w n & & C L I C K _ E V E N T _ N A M E = = = ' t o u c h e n d ' ) {
add ( 'mousedown' , callback , options ) ;
} else if ( CLICK_EVENT_NAME === 'touchend' ) {
const o = { . . . options , once : true } ;
const onTouchStart = ( e : TouchEvent ) = > {
const onTouchMove = ( e : TouchEvent ) = > {
remove ( 'touchmove' , onTouchMove , o ) ;
remove ( 'touchend' , onTouchEnd , o ) ;
} ;
const onTouchEnd = ( e : TouchEvent ) = > {
remove ( 'touchmove' , onTouchMove , o ) ;
callback ( e ) ;
if ( options . once ) {
remove ( 'touchstart' , onTouchStart ) ;
}
} ;
add ( 'touchend' , onTouchEnd , o ) ;
add ( 'touchmove' , onTouchMove , o ) ;
} ;
add ( 'touchstart' , onTouchStart ) ;
} else {
add ( CLICK_EVENT_NAME , callback , options ) ;
} * /
add ( CLICK_EVENT_NAME , callback , options ) ;
} ;
export const detachClickEvent = ( elem : HTMLElement , callback : ( e : TouchEvent | MouseEvent ) = > void , options? : AddEventListenerOptions ) = > {
if ( CLICK_EVENT_NAME === 'touchend' ) {
elem . removeEventListener ( 'touchstart' , callback , options ) ;
} else {
elem . removeEventListener ( CLICK_EVENT_NAME , callback , options ) ;
}
} ;
export const getSelectedNodes = ( ) = > {
const nodes : Node [ ] = [ ] ;
const selection = window . getSelection ( ) ;
for ( let i = 0 ; i < selection . rangeCount ; ++ i ) {
const range = selection . getRangeAt ( i ) ;
let { startContainer , endContainer } = range ;
if ( endContainer . nodeType !== 3 ) endContainer = endContainer . firstChild ;
while ( startContainer && startContainer !== endContainer ) {
nodes . push ( startContainer . nodeType === 3 ? startContainer : startContainer.firstChild ) ;
startContainer = startContainer . nextSibling ;
}
if ( nodes [ nodes . length - 1 ] !== endContainer ) {
nodes . push ( endContainer ) ;
}
}
// * filter null's due to <br>
return nodes . filter ( node = > ! ! node ) ;
} ;
/ * e x p o r t c o n s t i s S e l e c t i o n S i n g l e = ( i n p u t : E l e m e n t = d o c u m e n t . a c t i v e E l e m e n t ) = > {
const nodes = getSelectedNodes ( ) ;
const parents = [ . . . new Set ( nodes . map ( node = > node . parentNode ) ) ] ;
@ -547,46 +99,6 @@ export const getSelectedNodes = () => {
@@ -547,46 +99,6 @@ export const getSelectedNodes = () => {
return single ;
} ; * /
export const handleScrollSideEvent = ( elem : HTMLElement , side : 'top' | 'bottom' , callback : ( ) = > void , listenerSetter : ListenerSetter ) = > {
if ( isTouchSupported ) {
let lastY : number ;
const options = { passive : true } ;
listenerSetter . add ( elem , 'touchstart' , ( e ) = > {
if ( e . touches . length > 1 ) {
onTouchEnd ( ) ;
return ;
}
lastY = e . touches [ 0 ] . clientY ;
listenerSetter . add ( elem , 'touchmove' , onTouchMove , options ) ;
listenerSetter . add ( elem , 'touchend' , onTouchEnd , options ) ;
} , options ) ;
const onTouchMove = ( e : TouchEvent ) = > {
const clientY = e . touches [ 0 ] . clientY ;
const isDown = clientY < lastY ;
if ( side === 'bottom' && isDown ) callback ( ) ;
else if ( side === 'top' && ! isDown ) callback ( ) ;
lastY = clientY ;
//alert('isDown: ' + !!isDown);
} ;
const onTouchEnd = ( ) = > {
listenerSetter . removeManual ( elem , 'touchmove' , onTouchMove , options ) ;
listenerSetter . removeManual ( elem , 'touchend' , onTouchEnd , options ) ;
} ;
} else {
listenerSetter . add ( elem , 'wheel' , ( e ) = > {
const isDown = e . deltaY > 0 ;
//this.log('wheel', e, isDown);
if ( side === 'bottom' && isDown ) callback ( ) ;
else if ( side === 'top' && ! isDown ) callback ( ) ;
} , { passive : true } ) ;
}
} ;
/ * e x p o r t f u n c t i o n r a d i o s H a n d l e C h a n g e ( i n p u t s : H T M L I n p u t E l e m e n t [ ] , o n C h a n g e : ( v a l u e : s t r i n g ) = > v o i d ) {
inputs . forEach ( input = > {
input . addEventListener ( 'change' , ( ) = > {
@ -597,105 +109,4 @@ export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom',
@@ -597,105 +109,4 @@ export const handleScrollSideEvent = (elem: HTMLElement, side: 'top' | 'bottom',
} ) ;
} * /
export function isSendShortcutPressed ( e : KeyboardEvent ) {
if ( e . key === 'Enter' && ! isMobile && ! e . isComposing ) {
/ * i f ( e . c t r l K e y | | e . m e t a K e y ) {
this . messageInput . innerHTML += '<br>' ;
placeCaretAtEnd ( this . message )
return ;
} * /
if ( rootScope . settings . sendShortcut === 'enter' ) {
if ( e . shiftKey || e . ctrlKey || e . metaKey ) {
return ;
}
return true ;
} else {
const secondaryKey = isApple ? e.metaKey : e.ctrlKey ;
if ( e . shiftKey || ( isApple ? e.ctrlKey : e.metaKey ) ) {
return ;
}
if ( secondaryKey ) {
return true ;
}
}
}
return false ;
}
export function reflowScrollableElement ( element : HTMLElement ) {
element . style . display = 'none' ;
void element . offsetLeft ; // reflow
element . style . display = '' ;
}
export function isSelectionEmpty ( selection = window . getSelection ( ) ) {
if ( ! selection || ! selection . rangeCount ) {
return true ;
}
const selectionRange = selection . getRangeAt ( 0 ) ;
if ( ! selectionRange . toString ( ) || ! selectionRange . START_TO_END ) {
return true ;
}
return false ;
}
export function disableTransition ( elements : HTMLElement [ ] ) {
elements . forEach ( el = > el . classList . add ( 'no-transition' ) ) ;
doubleRaf ( ) . then ( ( ) = > {
elements . forEach ( el = > el . classList . remove ( 'no-transition' ) ) ;
} ) ;
}
export function toggleDisability ( elements : HTMLElement [ ] , disable : boolean ) {
if ( disable ) {
elements . forEach ( el = > el . setAttribute ( 'disabled' , 'true' ) ) ;
} else {
elements . forEach ( el = > el . removeAttribute ( 'disabled' ) ) ;
}
return ( ) = > toggleDisability ( elements , ! disable ) ;
}
export function canFocus ( isFirstInput : boolean ) {
return ! isMobileSafari || ! isFirstInput ;
}
export function htmlToDocumentFragment ( html : string ) {
var template = document . createElement ( 'template' ) ;
html = html . trim ( ) ; // Never return a text node of whitespace as the result
template . innerHTML = html ;
return template . content ;
}
export function htmlToSpan ( html : string ) {
const span = document . createElement ( 'span' ) ;
span . innerHTML = html ;
return span ;
}
export function replaceContent ( elem : HTMLElement , node : string | Node ) {
// * children.length doesn't count text nodes
const firstChild = elem . firstChild ;
if ( firstChild ) {
if ( elem . lastChild === firstChild ) {
firstChild . replaceWith ( node ) ;
} else {
elem . textContent = '' ;
elem . append ( node ) ;
}
} else {
elem . append ( node ) ;
}
}
export function setInnerHTML ( elem : HTMLElement , html : string ) {
elem . setAttribute ( 'dir' , 'auto' ) ;
elem . innerHTML = html ;
}
export default { } ;