@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>
162 lines (140 loc) • 3.9 kB
text/typescript
import { Elements, EventBusEvent } from '@ryusei/code';
import { Component } from '../../classes/Component/Component';
import { CLASS_CARETS } from '../../constants/classes';
import { EVENT_READONLY, EVENT_SELECTED, EVENT_SELECTING } from '../../constants/events';
import { CHANGED, COLLAPSED, SELECTED } from '../../constants/selection-states';
import { Editor } from '../../core/Editor/Editor';
import { assert, div, isIE, isMobile, rafThrottle } from '../../utils';
import { Selection } from '../Selection/Selection';
import { CustomCaret } from './CustomCaret';
/**
* The ID of the primary caret.
*
* @since 0.1.0
*/
export const PRIMARY_CARET_ID = 'primary';
/**
* The component for generating and handling carets.
*
* @since 0.1.0
*/
export class Caret extends Component {
/**
* The wrapper element that contains caret elements.
*/
private wrapper: HTMLDivElement;
/**
* Stores the all registered Caret instances.
*/
private carets: Record<string, CustomCaret> = {};
/**
* Holds the primary Caret instance.
*/
private primary: CustomCaret;
/**
* Mounts the component.
* Uses the native caret on IE and mobile devices.
*
* @internal
*
* @param elements - A collection of essential editor elements.
*/
mount( elements: Elements ): void {
super.mount( elements );
this.create();
if ( ! isIE() && ! isMobile() ) {
this.register( PRIMARY_CARET_ID );
this.primary = this.get( PRIMARY_CARET_ID );
this.listen();
}
}
/**
* Creates a wrapper element that contains carets.
*/
private create(): void {
this.wrapper = div( {
class : CLASS_CARETS,
role : 'presentation',
'aria-hidden': true,
}, this.elements.editor );
}
/**
* Listens to some events.
*/
private listen(): void {
const { editable } = this.elements;
const { primary, Editor } = this;
this.bind( editable, 'focus', () => {
if ( ! Editor.readOnly ) {
primary.show();
}
} );
this.bind( editable, 'blur', () => {
primary.hide();
} );
this.update = rafThrottle( this.update.bind( this ), true );
this.on( EVENT_READONLY, ( e, readOnly ) => {
if ( readOnly ) {
primary.hide();
} else {
if ( Editor.isFocused() ) {
this.update();
primary.show();
}
}
} );
this.on( EVENT_SELECTED, this.onSelected, this );
this.on( EVENT_SELECTING, this.update );
}
/**
* Called when the selection state is changed.
*
* @param e - An EventBusEvent object.
* @param Selection - A Selection instance.
*/
private onSelected( e: EventBusEvent<Editor>, Selection: Selection ): void {
if ( ! this.Editor.readOnly ) {
if ( Selection.is( CHANGED, COLLAPSED, SELECTED ) ) {
this.update();
}
}
}
/**
* Updates the primary caret position on the animation frame.
*/
private update(): void {
this.primary.move( this.Selection.get( false ).end );
}
/**
* Registers a new caret.
*
* @param id - The ID for the caret to register.
*
* @return The registered CustomCaret instance.
*/
register( id: string ): CustomCaret {
const { carets } = this;
assert( ! carets[ id ] );
const caret = new CustomCaret( this.Editor, id, this.wrapper );
carets[ id ] = caret;
return caret;
}
/**
* Returns the primary or the specific CustomCaret instance.
*
* @param id - Optional. A caret ID.
*
* @return A CustomCaret instance if available, or otherwise `undefined`.
*/
get( id = PRIMARY_CARET_ID ): CustomCaret | undefined {
return this.carets[ id ];
}
/**
* Returns the DOMRect object of the primary caret.
*
* @return A DOMRect object.
*/
get rect(): DOMRect | null {
return this.Selection.getRect( true );
}
}