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>

297 lines (246 loc) 7.69 kB
import { Elements, EventBusEvent, Position } from '@ryusei/code'; import { Component } from '../../classes/Component/Component'; import { LINE_BREAK } from '../../constants/characters'; import { CONTEXT_MENU_EDIT, CONTEXT_MENU_SELECTION, MAIN_CONTEXT_MENU_ID } from '../../constants/context-menu'; import { EVENT_CHANGE, EVENT_CHANGED, EVENT_CONTEXT_MENU_CLICKED, EVENT_COPY, EVENT_CUT, EVENT_KEYDOWN, EVENT_PASTE, } from '../../constants/events'; import { Editor } from '../../core/Editor/Editor'; import { count, includes, isIE, isUndefined, normalizeKey, prevent, toArray } from '../../utils'; import { ContextMenu } from '../ContextMenu/ContextMenu'; import { Clipboard } from './Clipboard'; /** * The class for editing the code. * * @since 0.1.0 */ export class Edit extends Component { /** * Indicates whether lines has been deleted by an input or not. */ private deletedByInput: boolean; /** * Holds the Clipboard instance. */ private clipboard = new Clipboard(); /** * Initializes the component. * * @internal * * @param elements - A collection of essential editor elements. */ mount( elements: Elements ): void { super.mount( elements ); this.register(); this.listen(); } /** * Listens to some events. */ private listen(): void { const { editable } = this.elements; this.on( EVENT_KEYDOWN, this.onKeydown, this ); this.bind( editable, 'paste', this.onPaste, this ); this.bind( editable, 'copy cut', e => { this[ e.type ](); } ); this.bind( editable, 'dragover drop paste cut', e => { prevent( e, true ); } ); this.on( EVENT_CONTEXT_MENU_CLICKED, this.onMenuClicked, this ); if ( isIE() ) { this.bind( editable, 'compositionstart', e => { if ( this.deletedByInput ) { prevent( e, true ); } } ); } } /** * Called when any key is pressed. * * @param e - An EventBusEvent object. * @param ke - A KeyboardEvent object. */ private onKeydown( e: EventBusEvent<Editor>, ke: KeyboardEvent ): void { const { Selection } = this; const key = normalizeKey( ke.key ); const isKey = ( keys: string | string[] ) => includes( toArray( keys ), key ); this.deletedByInput = false; if ( this.Keymap.matches( ke, 'selectAll' ) ) { Selection.selectAll(); return prevent( ke, true ); } if ( ke.altKey || ke.metaKey || ke.ctrlKey ) { return; } if ( Selection.isMultiline() ) { if ( key.length === 1 || isKey( [ 'Process', 'Enter' ] ) ) { this.delete(); this.deletedByInput = true; } else if ( isKey( [ 'Delete', 'Backspace' ] ) ) { this.delete(); prevent( ke ); } } } /** * Called when the context menu item is clicked. * * @param e - An EventBusEvent object. * @param ContextMenu - A ContextMenu instance. * @param group - A group ID. * @param id - The ID of the clicked item. */ private onMenuClicked( e: EventBusEvent<Editor>, ContextMenu: ContextMenu, group: string, id: string ): void { if ( group === MAIN_CONTEXT_MENU_ID ) { const { Selection } = this; if ( id === 'copy' || id === 'cut' ) { if ( ! this.isSelected() ) { Selection.selectLine( undefined, id === 'copy', true ); } this[ id ](); } else if ( id === 'paste' ) { this.clipboard.paste( this.paste.bind( this ) ); } else if ( id === 'selectAll' ) { Selection.selectAll(); } } } /** * Called when the text is being pasted to the editor. * * @param e - A ClipboardEvent object. */ private onPaste( e: ClipboardEvent ): void { const string = ( e.clipboardData || window[ 'clipboardData' ] ).getData( 'text' ); if ( string ) { this.paste( string ); } prevent( e ); } /** * Registers items to the context menu. */ private register(): void { const { ContextMenu } = this; ContextMenu.register( MAIN_CONTEXT_MENU_ID, 'edit', CONTEXT_MENU_EDIT ); ContextMenu.register( MAIN_CONTEXT_MENU_ID, 'selection', CONTEXT_MENU_SELECTION ); } /** * Checks if some texts are selected or not. * Be aware that this is not same with negating getSelection().isCollapsed. * * @return `true` if some texts are selected, or otherwise `false`. */ private isSelected(): boolean { return ! this.Selection.isCollapsed(); } /** * Checks if the Editor is editable or not. * * @return `true` if the Editor is editable. */ private isEditable(): boolean { return ! this.Editor.readOnly; } /** * Deletes the selected text. Nothing will happen when the selection is collapsed. */ delete(): void { if ( this.isSelected() ) { this.paste( '', 'delete' ); } } /** * Pastes the provided text at the current position. * * @param string - A string to paste. * @param type - Optional. Specifies the input type. */ paste( string: string, type = 'paste' ): void { if ( ! this.isEditable() ) { return; } if ( type === 'paste' ) { this.emit( EVENT_PASTE, string ); } const { Selection, Code } = this; const { start, end } = Selection.get(); const size = count( string, LINE_BREAK ) + 1; const startRow = start[ 0 ]; const endRow = startRow + size - 1; const endLine = string.slice( string.lastIndexOf( LINE_BREAK ) + 1 ); const col = endLine.length + ( size > 1 ? 0 : start[ 1 ] ); const position = [ endRow, col ] as Position; this.emit( EVENT_CHANGE, type ); Code.replaceRange( start, end, string ); this.Sync.sync( startRow, endRow, endRow ); Selection.set( position ); this.emit( EVENT_CHANGED, type ); } /** * Copies the provided text to the clipboard. * If the text is not provided, this method tries to copy the current selection. * * @param string - Optional. A string to copy. * @param skipSelection - Optional. Whether to restore the selection range after copy or not. */ copy( string?: string, skipSelection?: boolean ): void { const { failedToCopy } = this.i18n; const onFailed = () => { if ( this.require( 'Dialog' ) ) { this.invoke( 'Dialog', 'message', failedToCopy ); } else { alert( this.i18n.failedToCopy ); } }; const copySelection = isUndefined( string ); string = copySelection ? this.Selection.toString() : string; this.emit( EVENT_COPY, string ); const { Selection } = this; const range = Selection.get( false ); this.clipboard.copy( string, onFailed ); if ( ! skipSelection ) { Selection.set( range.start, range.end ); } } /** * Cuts the selected code. Nothing will happen if the selection is collapsed. */ cut(): void { if ( this.isSelected() && this.isEditable() ) { this.emit( EVENT_CUT ); this.copy( undefined, true ); this.delete(); } } /** * Cuts the current line. */ cutLine(): void { if ( ! this.isEditable() ) { return; } this.emit( EVENT_CUT ); const { Selection } = this; const { start: [ startRow ] } = Selection.get(); const position = [ startRow, 0 ] as Position; this.View.jump( startRow ); Selection.selectLine( startRow, false ); this.copy( undefined, true ); Selection.update( position ); this.emit( EVENT_CHANGE ); this.Code.replaceLines( startRow, startRow, '' ); this.Sync.sync( startRow, startRow ); Selection.set( position ); this.emit( EVENT_CHANGED ); } }