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>

244 lines (214 loc) 6.71 kB
import { Elements, EventBusEvent, OffsetPosition, Position } from '@ryusei/code'; import { Component } from '../../classes/Component/Component'; import { CLASS_LINE } from '../../constants/classes'; import { EVENT_FONT_LOADED, EVENT_MOUNT, EVENT_RESIZE, EVENT_SCROLL_HEIGHT_CHANGED, EVENT_SCROLLED, EVENT_WINDOW_SCROLL, } from '../../constants/events'; import { Editor } from '../../core/Editor/Editor'; import { clamp, div, rect, remove, round, styles } from '../../utils'; import { MeasureText } from './MeasureText'; /** * The class for measuring offset positions and caches some values. * * @since 0.1.0 */ export class Measure extends Component { /** * Caches the lineHeight. */ private lineHeightCache: number; /** * Caches the DOMRect objects of some elements. */ private rectCaches: { editor?: DOMRect, scroller?: DOMRect, container?: DOMRect } = {}; /** * Keeps the current CSS font settings. */ private font: string; /** * Holds the MeasureText instance. */ private measureText: MeasureText; /** * An object with padding values as `{ top, right, bottom, left }`; */ padding: { top: number, right: number, bottom: number, left: number }; /** * The Measure constructor. * * @param Editor - An Editor instance. */ constructor( Editor: Editor ) { super( Editor ); this.on( EVENT_MOUNT, this.onMount, this, 0 ); } /** * Called just before components are mounted. * This component must be initialized earlier than other components. * * @param e - An EventBusEvent object. * @param elements - A collection of essential editor elements. */ private onMount( e: EventBusEvent<Editor>, elements: Elements ): void { this.elements = elements; this.createMeasureText(); this.updatePadding(); this.listen(); } /** * Listens to some events. * The resize handler must be executed after the Style update listener and before others. */ private listen(): void { this.on( EVENT_RESIZE, () => { this.lineHeightCache = 0; this.updatePadding(); this.createMeasureText(); this.clearRectCaches(); }, null, 1 ); this.on( EVENT_FONT_LOADED, () => { this.measureText.clear(); }, null, 1 ); this.on( [ EVENT_SCROLL_HEIGHT_CHANGED, EVENT_SCROLLED, EVENT_WINDOW_SCROLL ], this.clearRectCaches, this, 1 ); } /** * Updates the cache of the padding. */ private updatePadding(): void { const { editor } = this.elements; const line = div( CLASS_LINE, editor ); this.padding = { top : parseFloat( styles( editor, 'paddingTop' ) ) || 0, bottom: parseFloat( styles( editor, 'paddingBottom' ) ) || 0, left : parseFloat( styles( line, 'paddingLeft' ) ) || 0, right : parseFloat( styles( line, 'paddingRight' ) ) || 0, }; remove( line ); } /** * Creates a `MeasureText` instance only when the font settings are changed. */ private createMeasureText() { const font = this.buildCSSFont(); if ( this.font !== font ) { this.measureText = new MeasureText( font ); this.font = font; } } /** * Returns the CSS font string of the current environment. * * @return A built string. */ private buildCSSFont(): string { const { lines } = this.elements; return `${ styles( lines, 'fontSize' ) } ${ styles( lines, 'fontFamily' ) }`; } /** * Clears the all rect caches. */ private clearRectCaches(): void { this.rectCaches = {}; } /** * Returns the top position of the line at the specified row. * This clamps the row index from 0 and the total length of lines. * * @param row - A row index. * * @return A top position in pixel. */ getTop( row: number ): number { return clamp( row, 0, this.lines.length - 1 ) * this.lineHeight; } /** * Returns the bottom position of the line at the specified row. * This clamps the row index from 0 and the total length of lines. * * @param row - A row index. * * @return A bottom position in pixel. */ getBottom( row: number ): number { const { Code } = this; const isLast = row >= Code.size - 1; return this.getTop( row + 1 ) + ( isLast ? this.lineHeight : 0 ); } /** * Computes the closest row index to the offset `top` position. * * @param top - A offset position. * * @return The closest row index to the offset position. */ closest( top: number ): number { const row = round( ( top - this.padding.top ) / this.lineHeight ); return clamp( row, 0, this.lines.length - 1 ); } /** * Measures the provided string and returns the width. * This method caches each width of the character in the string for performance. * * @param string - A string to measure. * @param useCache - Optional. Determines whether to use the cached width or not. * * @return The width of the string. */ measureWidth( string: string, useCache = true ): number { return this.measureText.measure( string, useCache ); } /** * Converts the passed position to the OffsetPosition object as `{ top: number, left: number }`. * * @param position - A position to convert. * * @return An object literal with top and left positions. */ getOffset( position: Position ): OffsetPosition { const { padding } = this; const line = position[ 0 ] === this.Selection.focus[ 0 ] ? this.Input.value : this.Code.getLine( position[ 0 ] ); // console.log( line.slice( 0, position[ 1 ] ) ); return { top : this.getTop( position[ 0 ] ) + padding.top, left: this.measureWidth( line.slice( 0, position[ 1 ] ) ) + padding.left, }; } /** * Returns a DOMRect object of the editor element. * * @return A DOMRect object. */ get editorRect(): DOMRect { return ( this.rectCaches.editor = this.rectCaches.editor || rect( this.elements.editor ) ); } /** * Returns a DOMRect object of the scroller element. * * @return A DOMRect object. */ get scrollerRect(): DOMRect { return ( this.rectCaches.scroller = this.rectCaches.scroller || rect( this.elements.scroller ) ); } /** * Returns a DOMRect object of the container element. * * @return A DOMRect object. */ get containerRect(): DOMRect { return ( this.rectCaches.container = this.rectCaches.container || rect( this.elements.container ) ); } /** * Returns the editor line height in pixel. * * @return The line height in pixel. */ get lineHeight(): number { return ( this.lineHeightCache = this.lineHeightCache || parseFloat( styles( this.elements.editor, 'lineHeight' ) ) ); } }