UNPKG

@ryusei/code

Version:

<div align="center"> <a href="https://code.ryuseijs.com"> <img alt="RyuseiCode" src="https://code.ryuseijs.com/images/svg/logo.svg" width="70"> </a>

211 lines (181 loc) 5.67 kB
import { AutoCloseConfig, Elements } from '@ryusei/code'; import { CATEGORY_STRING } from '@ryusei/light'; import { Component } from '../../classes/Component/Component'; import { EVENT_CHANGED, EVENT_KEYDOWN } from '../../constants/events'; import { compare, isArray, isFunction, isString, normalizeKey, prevent } from '../../utils'; /** * The component for auto closing brackets. * * @since 0.1.0 */ export class AutoClose extends Component { /** * Initializes the component. * * @param elements - A collection of essential elements. */ mount( elements: Elements ): void { super.mount( elements ); this.on( EVENT_KEYDOWN, ( e, ke ) => { this.skip( ke ); this.remove( ke ); } ); this.on( EVENT_CHANGED, ( e, type ) => { if ( type === 'input' ) { this.close(); } } ); } /** * Closes the entered opening character. */ private close(): void { const { Input } = this; if ( ! Input.composing ) { const index = this.getChars( false ).indexOf( Input.get().key ); if ( index > -1 && this.validate( index, 'close' ) ) { Input.apply( { type : 'autoClose', insertion: this.getClosingString( index ), offset : this.getOffset( index ), } ); } } } /** * Skips the entered close character if the next character is already the closing character. * * @param e - A KeyboardEvent object. */ private skip( e: KeyboardEvent ): void { const { Input } = this; if ( ! Input.composing ) { const closingChars = this.getChars( true ); const index = closingChars.indexOf( normalizeKey( e.key ) ); if ( index > -1 && this.validate( index, 'skip' ) ) { if ( closingChars[ index ] === Input.char() ) { const { Selection, Selection: { focus } } = this; Selection.set( [ focus[ 0 ], focus[ 1 ] + 1 ] ); prevent( e ); } } } } /** * Automatically removes the paired characters when the backspace key is pressed. * * @param e - A KeyboardEvent object. */ private remove( e: KeyboardEvent ): void { const { Input } = this; if ( e.key === 'Backspace' ) { const index = this.getChars( false ).indexOf( Input.char( Input.col - 1 ) ); if ( index > -1 && this.validate( index, 'remove' ) ) { if ( this.getChars( true )[ index ] === Input.char() ) { const { Selection, Selection: { focus } } = this; Input.value = Input.before + Input.after.slice( 1 ); Selection.set( focus ); } } } } /** * Returns an array with opening/closing characters. * * @param closing - Determines whether to get closing or opening characters. * * @return An array with characters. */ private getChars( closing: boolean ): string[] { return this.getConfig().map( chars => { const value = chars[ closing ? 1 : 0 ]; return isString( value ) ? value : ''; } ); } /** * Returns a closing string. * * @param index - A config index. * * @return A closing string. This may be empty. */ private getClosingString( index: number ): string { const config = this.getConfig()[ index ]; const closer = config && config[ 1 ]; return isFunction( closer ) ? closer( this.Editor ) : closer || ''; } /** * Returns a number of characters to offset. * * @param index - A config index. * * @return The number of characters to offset. */ private getOffset( index: number ): number { const config = this.getConfig()[ index ]; const data = config && config[ 2 ]; return data ? data.offset || 0 : 0; } /** * Executes the validator defined by the language data. * * @param index - A config index. * @param key - A key of the validator. * * @return `true` if the input satisfies the validator, or otherwise `false`. */ private validate( index: number, key: 'close' | 'skip' | 'remove' ): boolean { const { Scope } = this; const config = this.getConfig()[ index ]; const data = config[ 2 ]; if ( ! data ) { return true; } const validator = data[ key ]; if ( isFunction( validator ) ) { return validator( this.Editor, config ); } if ( isString( validator ) ) { if ( validator === '@quotes' ) { return this.validateQuote( key ); } return false; } if ( isArray( validator ) ) { return Scope.isIn( validator ); } return validator; } /** * Determines whether to proceed completion of quotes or not. * - RegExp: checks the string after the input quote. * * @param key - The key of the validator. * * @return `true` if the completion process should be proceeded, or otherwise `false`. */ private validateQuote( key: 'close' | 'skip' | 'remove' ): boolean { const { start } = this.Selection.get(); const { Input } = this; const currInfo = this.lines.getInfoAt( start ); const prevInfo = Input.info; if ( currInfo ) { if ( currInfo.category === CATEGORY_STRING || prevInfo && prevInfo.category === CATEGORY_STRING ) { if ( key === 'skip' || key === 'remove' ) { return compare( start, [ start[ 0 ], currInfo.to - 1 ] ) === 0; } return false; } } const { after } = Input; return ! this.Scope.isIn( 'comment' ) && ( !after || /^\s/.test( after ) ); } /** * Returns the config array. * * @return A config array. */ private getConfig(): AutoCloseConfig[] { return this.getLanguage().autoClose || []; } }