import { RIGHT_TO_LEFT } from '@proton/shared/lib/constants' ; import { getElement } from '@proton/shared/lib/helpers/dom' ; import { LinkData , SquireType } from './interface' ; import { DEFAULT_LINK , DEFAULT_BACKGROUND , RGB_REGEX , FONT_FACE , DEFAULT_FONT_COLOR , FONT_SIZES , EMBEDDABLE_TYPES , } from './squireConfig' ; const pathInfoTests = { blockquote : /^blockquote$/ , italic : /^i$/ , underline : /^u$/ , bold : /^b$/ , unorderedList : /^ul$/ , orderedList : /^ol$/ , listElement : /^li$/ , alignCenter : /\.align-center/ , alignLeft : /\.align-left/ , alignRight : /\.align-right/ , alignJustify : /\.align-justify/ , } ; const REGEX_DIRECTION = /\[(dir=(rtl|ltr))]/g ; const testPresenceInSelection = ( squire , format , validation ) => validation . test ( squire . getPath ( ) ) || squire . hasFormat ( format ) ; const toggleFormat = ( tag , test , addAction , removeAction ) => ( squire ) => { if ( testPresenceInSelection ( squire , tag , test ) ) { ( squire as any ) [ removeAction ] ( ) ; } else { ( squire as any ) [ addAction ] ( ) ; } } ; const toggleList = ( typeIsOrdered ) => ( squire ) => { const testOrdered = testPresenceInSelection ( squire , 'OL' , />OL\b/ ) ; const testUnordered = testPresenceInSelection ( squire , 'UL' , />UL\b/ ) ; const action = typeIsOrdered ? 'makeOrderedList' : 'makeUnorderedList' ; if ( ( ! testOrdered && ! testUnordered ) || ( typeIsOrdered && testUnordered ) || ( ! typeIsOrdered && testOrdered ) ) { squire [ action ] ( ) ; } else { squire . removeList ( ) ; } } ; /**
 * Get the image from the current selection.
 */ const getSelectedImg = ( squire ) => { const range = squire . getSelection ( ) ; const ancestor = getElement ( range . commonAncestorContainer ) ; return ancestor ?. querySelector ( 'img' ) ; } ; const decimalToHex = ( decimal ) => parseInt ( decimal , 10 ) . toString ( 16 ) ; const rgbToHex = ( rgb = '' , defaultValue ) => { const result = RGB_REGEX . exec ( rgb ) ; if ( ! result || result . length < 4 ) { return defaultValue ; } return `#${ decimalToHex ( result [ 1 ] ) }${ decimalToHex ( result [ 2 ] ) }${ decimalToHex ( result [ 3 ] ) }` ; } ; /**
 * Strip away the direction attribute from a squire path
 */ const stripDirectionAttribute = ( path = '' ) => { return path . replace ( REGEX_DIRECTION , '' ) ; } ; /**
 * Run the callback at any cursor change in Squire
 * Returns an unsubscribe functions meant to be the return value of a useEffect
 */ export const listenToCursor = ( squire , callback ) => { if ( ! squire ) { return ; } squire . addEventListener ( 'click' , callback ) ; squire . addEventListener ( 'keyup' , callback ) ; squire . addEventListener ( 'pathChange' , callback ) ; callback ( ) ; return ( ) => { squire . removeEventListener ( 'click' , callback ) ; squire . removeEventListener ( 'keyup' , callback ) ; squire . removeEventListener ( 'pathChange' , callback ) ; } ; } ; export const toggleBold = toggleFormat ( 'b' , />B\b/ , 'bold' , 'removeBold' ) ; export const toggleItalic = toggleFormat ( 'i' , />I\b/ , 'italic' , 'removeItalic' ) ; export const toggleUnderline = toggleFormat ( 'u' , />U\b/ , 'underline' , 'removeUnderline' ) ; export const toggleOrderedList = toggleList ( true ) ; export const toggleUnorderedList = toggleList ( false ) ; export const toggleBlockquote = toggleFormat ( 'blockquote' , />BLOCKQUOTE\b/ , 'increaseQuoteLevel' , 'decreaseQuoteLevel' ) ; export const getPathInfo = ( squire ) => { const path = stripDirectionAttribute ( squire . getPath ( ) ) ; const pathList = path . toLowerCase ( ) . split ( '>' ) ; return Object . keys ( pathInfoTests ) . reduce ( ( acc , test ) => { acc [ test ] = pathList . some ( ( path ) => pathInfoTests [ test ] . test ( path ) ) ; return acc ; } , { } as { [ test ] : boolean ; } ) ; } ; export const getFontLabel = ( font ) => Object . entries ( FONT_FACE ) . find ( ( [ , value ] ) => value === font ) ?. [ 0 ] ; export const getFontFaceAtCursor = ( squire ) => { const { family } = squire . getFontInfo ( ) ; const first = family ?. split ( ',' ) [ 0 ] . replace ( /"/g , '' ) . trim ( ) ; return Object . entries ( FONT_FACE ) . find ( ( [ , value ] ) => first && value . includes ( first ) ) ?. [ 1 ] ; } ; export const getFontSizeAtCursor = ( squire ) => { const { size } = squire . getFontInfo ( ) ; const stringValue = size ? /(\d+)px/ . exec ( size ) ?. [ 1 ] : undefined ; const value = Number ( stringValue ) ; if ( ! value ) { return undefined ; } return FONT_SIZES . reduce ( ( acc , currentValue ) => ( Math . abs ( value - currentValue ) < Math . abs ( value - acc ) ? currentValue : acc ) , FONT_SIZES [ 0 ] ) ; } ; /**
 * Get font and background color of the selection
 */ export const getColorsAtCursor = ( squire ) => { const { color , backgroundColor } = squire . getFontInfo ( ) ; return { font : rgbToHex ( color , DEFAULT_FONT_COLOR ( ) ) , bg : rgbToHex ( backgroundColor , DEFAULT_BACKGROUND ( ) ) } ; } ; /**
 * Get the current link at the cursor or the current selected item
 */ export const getLinkAtCursor = ( squire ) : LinkData => { const range = squire . getSelection ( ) ; const ancestor = getElement ( range . commonAncestorContainer ) ; const a = ancestor ?. closest ( 'a' ) ; return { link : a ?. href || DEFAULT_LINK , title : a ?. textContent || range . toString ( ) || DEFAULT_LINK , } ; } ; /**
 * Create a link inside the editor
 */ export const makeLink = ( squire , { link = '' , title } ) => { const image = getSelectedImg ( squire ) ; const a = getElement ( squire . getSelection ( ) . commonAncestorContainer ) ?. closest ( 'a' ) ; const range = squire . getDocument ( ) . createRange ( ) ; const selection = squire . getDocument ( ) . getSelection ( ) ; if ( a ) { range . selectNodeContents ( a ) ; selection ?. removeAllRanges ( ) ; selection ?. addRange ( range ) ; squire . setSelection ( range ) ; } squire . makeLink ( link , { target : '_blank' , title : link , rel : 'nofollow' , } ) ; if ( ! image ) { const ancestor = getElement ( squire . getSelection ( ) . commonAncestorContainer ) ; const linkElement = ancestor ?. closest ( 'a' ) || ancestor ?. querySelector ( 'a' ) ; if ( linkElement ) { linkElement . textContent = title || link ; } } } ; /**
 * Insert an image inside the editor
 */ export const insertImage = ( squire , url , inputAttributes = { } ) => { const attributes = { ... inputAttributes , class : `${ inputAttributes . class ? `${ inputAttributes . class } ` : '' }proton-embedded` , } ; squire . focus ( ) ; squire . insertImage ( url , attributes ) ; } ; const rightToLeftToString = ( rtl ) => { switch ( rtl ) { case RIGHT_TO_LEFT . ON : return 'rtl' ; default : case RIGHT_TO_LEFT . OFF : return 'ltr' ; } } ; export const setTextDirection = ( squire , direction ) => { squire . setTextDirection ( rightToLeftToString ( direction ) ) ; } ; /**
 * The default text direction function sets focus to the editor, which breaks the
 * custom focus used by the composer when setting the default text direction to be RTL
 */ export const setTextDirectionWithoutFocus = ( squire , direction ) => { squire . forEachBlock ( ( block ) => { if ( direction ) { block . setAttribute ( 'dir' , rightToLeftToString ( direction ) ) ; } else { block . removeAttribute ( 'dir' ) ; } } , true ) ; } ; /**
 * Handler for squire paste event
 * Deals with pasting images
 */ export const pasteFileHandler = ( event , onAddImages ) => { const { clipboardData } = event ; const files = Array . from ( clipboardData ?. files || ( clipboardData ?. items as any as FileList ) || [ ] ) ; if ( files . length && files . every ( ( file ) => EMBEDDABLE_TYPES . includes ( file . type ) ) ) { onAddImages ( files ) ; } } ; export const scrollIntoViewIfNeeded = ( squire ) => { const document = squire . getDocument ( ) ; const container = document . querySelector ( 'html' ) ; const containerRect = container ?. getBoundingClientRect ( ) ; const scrollable = document . body ; const cursorRect = squire . getCursorPosition ( ) ; if ( ! container || ! containerRect || ! scrollable || ! cursorRect ) { return ; } if ( cursorRect . bottom > containerRect . height ) { const paddingValue = window . getComputedStyle ( scrollable ) . getPropertyValue ( 'paddingBottom' ) ; const paddingPx = parseInt ( /(\d+)px/ . exec ( paddingValue ) ?. [ 1 ] || '8' , 10 ) ; scrollable . scroll ( { top : scrollable . scrollTop + cursorRect . bottom - containerRect . height + paddingPx } ) ; } } ; /**
 * Hacky implementation but setting HTML content in Squire will reset the undo history
 * Unfortunately also move the cursor to the beginning of the document
 */ export const clearUndoHistory = ( squire ) => { squire . setHTML ( squire . getHTML ( ) ) ; } ;