import css from 'tui-code-snippet/domUtil/css' ; import addClass from 'tui-code-snippet/domUtil/addClass' ; import removeClass from 'tui-code-snippet/domUtil/removeClass' ; import hasClass from 'tui-code-snippet/domUtil/hasClass' ; import toArray from 'tui-code-snippet/collection/toArray' ; import inArray from 'tui-code-snippet/array/inArray' ; import { isPositionInBox , removeNode , cls } from '@/utils/dom' ; import type { Emitter } from '@toast-ui/editor' ; export const WRAPPER_CLASS_NAME = 'code-block-language' ; export const INPUT_CLASS_NANE = 'code-block-language-input' ; export const LIST_CLASS_NAME = 'code-block-language-list' ; export const LANG_ATTR = 'data-language' ; const CODE_BLOCK_PADDING = 10 ; function getButtonsHTML ( languages ) { return languages . map ( ( language ) => `<button type="button" data-language="${ language }">${ language }</button>` ) . join ( '' ) ; } export class LanguageSelectBox { private eventEmitter : Emitter ; private languages : string [ ] ; private wrapper ! : HTMLElement ; private input ! : HTMLInputElement ; private list ! : HTMLElement ; private buttons : Element [ ] = [ ] ; private currentButton ! : Element ; private prevStoredLanguage = '' ; constructor ( eventEmitter , languages ) { this . eventEmitter = eventEmitter ; this . languages = languages ; this . createElement ( ) ; this . bindDOMEvent ( ) ; this . bindEvent ( ) ; } private createElement ( ) { this . wrapper = document . createElement ( 'div' ) ; addClass ( this . wrapper , cls ( WRAPPER_CLASS_NAME ) ) ; this . createInputElement ( ) ; this . createLanguageListElement ( ) ; document . querySelector < HTMLElement > ( '.toastui-editor.ww-mode' ) ! . appendChild ( this . wrapper ) ; this . hide ( ) ; } private createInputElement ( ) { const wrapper = document . createElement ( 'span' ) ; addClass ( wrapper , cls ( INPUT_CLASS_NANE ) ) ; const input = document . createElement ( 'input' ) ; input . type = 'text' ; input . setAttribute ( 'maxlength' , '20' ) ; this . input = input ; wrapper . appendChild ( this . input ) ; this . wrapper . appendChild ( wrapper ) ; } private createLanguageListElement ( ) { this . list = document . createElement ( 'div' ) ; addClass ( this . list , cls ( LIST_CLASS_NAME ) ) ; const buttonsContainer = document . createElement ( 'div' ) ; addClass ( buttonsContainer , 'buttons' ) ; buttonsContainer . innerHTML = getButtonsHTML ( this . languages ) ; this . buttons = toArray ( buttonsContainer . children ) ; this . list . appendChild ( buttonsContainer ) ; this . wrapper . appendChild ( this . list ) ; this . activateButtonByIndex ( 0 ) ; this . hideList ( ) ; } private bindDOMEvent ( ) { this . wrapper . addEventListener ( 'mousedown' , this . onSelectToggleButton ) ; this . input . addEventListener ( 'keydown' , this . handleKeydown ) ; this . input . addEventListener ( 'focus' , ( ) => this . activateSelectBox ( ) ) ; this . input . addEventListener ( 'blur' , ( ) => this . inactivateSelectBox ( ) ) ; this . list . addEventListener ( 'mousedown' , this . onSelectLanguageButtons ) ; } private bindEvent ( ) { this . eventEmitter . listen ( 'showCodeBlockLanguages' , this . showLangugaeSelectBox ) ; } private onSelectToggleButton = ( ev ) => { const target = ev . target as HTMLElement ; const style = getComputedStyle ( target , ':after' ) ; const { offsetX , offsetY } = ev ; if ( isPositionInBox ( style , offsetX , offsetY ) ) { ev . preventDefault ( ) ; this . toggleFocus ( ) ; } } ; private onSelectLanguageButtons = ( ev ) => { const target = ev . target as HTMLElement ; const language = target . getAttribute ( LANG_ATTR ) ; if ( language ) { this . selectLanguage ( language ) ; } } ; private handleKeydown = ( ev ) => { const { key } = ev ; if ( key === 'ArrowUp' ) { this . selectPrevLanguage ( ) ; ev . preventDefault ( ) ; } else if ( key === 'ArrowDown' ) { this . selectNextLanguage ( ) ; ev . preventDefault ( ) ; } else if ( key === 'Enter' || key === 'Tab' ) { this . storeInputLanguage ( ) ; ev . preventDefault ( ) ; } else { this . hideList ( ) ; } } ; private showLangugaeSelectBox = ( { top , right } , language ) => { if ( language ) { this . setLanguage ( language ) ; } this . show ( ) ; const { width } = this . input . parentElement ! . getBoundingClientRect ( ) ; css ( this . wrapper ! , { top : `${ top + CODE_BLOCK_PADDING }px` , left : `${ right - width - CODE_BLOCK_PADDING }px` , } ) ; this . toggleFocus ( ) ; } ; private activateSelectBox ( ) { addClass ( this . wrapper , 'active' ) ; css ( this . list , { display : 'block' } ) ; } private inactivateSelectBox ( ) { this . input ! . value = this . prevStoredLanguage ; removeClass ( this . wrapper , 'active' ) ; this . hideList ( ) ; } private toggleFocus ( ) { if ( hasClass ( this . wrapper , 'active' ) ) { this . input . blur ( ) ; } else { this . input . focus ( ) ; } } private storeInputLanguage ( ) { const selectedLanguage = this . input ! . value ; this . setLanguage ( selectedLanguage ) ; this . hideList ( ) ; this . eventEmitter . emit ( 'selectLanguage' , selectedLanguage ) ; } private activateButtonByIndex ( index ) { if ( this . currentButton ) { removeClass ( this . currentButton , 'active' ) ; } if ( this . buttons . length ) { this . currentButton = this . buttons [ index ] ; this . input ! . value = this . currentButton . getAttribute ( LANG_ATTR ) ! ; addClass ( this . currentButton , 'active' ) ; this . currentButton . scrollIntoView ( ) ; } } private selectLanguage ( selectedLanguage ) { this . input ! . value = selectedLanguage ; this . storeInputLanguage ( ) ; } private selectPrevLanguage ( ) { let index = inArray ( this . currentButton , this . buttons ) - 1 ; if ( index < 0 ) { index = this . buttons . length - 1 ; } this . activateButtonByIndex ( index ) ; } private selectNextLanguage ( ) { let index = inArray ( this . currentButton , this . buttons ) + 1 ; if ( index >= this . buttons . length ) { index = 0 ; } this . activateButtonByIndex ( index ) ; } private hideList ( ) { css ( this . list , { display : 'none' } ) ; } show ( ) { css ( this . wrapper ! , { display : 'inline-block' } ) ; } hide ( ) { css ( this . wrapper ! , { display : 'none' } ) ; } setLanguage ( language ) { this . prevStoredLanguage = language ; this . input ! . value = language ; const item = this . buttons . filter ( ( button ) => button . getAttribute ( LANG_ATTR ) === language ) ; if ( item . length ) { const index = inArray ( item [ 0 ] , this . buttons ) ; this . activateButtonByIndex ( index ) ; } } destroy ( ) { removeNode ( this . wrapper ) ; this . eventEmitter . removeEventHandler ( 'showCodeBlockLanguages' , this . showLangugaeSelectBox ) ; } }