RyuseiLight

RyuseiCode is a lightweight, extensible and accessible code editor.
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 { 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.
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 { 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 = {};

  /**
   * Holds the primary Caret instance.
   */
  private primary: CustomCaret;

  /**
   * Mounts the component.
   * Uses the native caret on IE and mobile devices.
   *
   * @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 ) );

    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, 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 - An ID for the caret to register.
   *
   * @return A registered Caret 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 particular caret.
   *
   * @param id - Optional. A caret ID.
   *
   * @return A Caret instance if available, or otherwise `undefined`.
   */
  get( id = PRIMARY_CARET_ID ): CustomCaret | undefined {
    return this.carets[ id ];
  }

  /**
   * Returns the DOMRect object of the native caret.
   *
   * @return A DOMRect object.
   */
  get rect(): DOMRect | null {
    return this.Selection.getRect( true );
  }
}

Features

Lightweight

You can start with the default small package that is about 70Kb (24Kb gzipped), including Indentation, History (Undo/Redo), and Shortcuts extensions.

Accessibility Friendly

By using a "contenteditable" element, screen readers are able to read the current and selected code for visually impaired users, which promises your site accessibility.

Modular and Extensible

The editor functionality can be extended by adding modular extensions. You can build your custom editor by picking them out or creating your own extensions.

Mobiles and IE

The UX is not perfect, but the editor works on mobile devices thanks to the "contenteditable". It also works on the IE11 with some limitations.
  • Highlighting the current line
  • Line numbers
  • Auto closing paired characters
  • Undoing and redoing code changes
  • Increasing and decreasing the indent level of selected lines
  • Commenting out selected lines with line or block comments by shortcuts
  • Highlighting matched brackets
  • Search and replace toolbar
  • Finding words in match case, whole word and regexp modes
  • Jump toolbar to go to the specific line
  • Resizing the editor size by drag
  • Custom dialog
  • Custom context menus
  • Text wrapping
  • Code hint
  • Huge code over 20,000 lines

Examples

Default

Only essential components, Dialog, Indentation, History, and Shortcut, are included, which would be enough for editing small code snippet.

console.log( 'Hello!' );
console.log( 'Hello!' );

Full Featured

All extensions are included.

Ctrl+ZUndo changes
Ctrl+Shift+ZRedo changes
Ctrl+FActivate the Search toolbar
Ctrl+Shift+FActivate the Replace toolbar
Ctrl+GActivate the Jump toolbar
Ctrl+CCopy the current line when the selection is collapsed
Ctrl+XCut the current line when the selection is collapsed
Ctrl+/Comment out and uncomment selected lines with line comments
Ctrl+Shift+/Comment out and uncomment selected lines with block comments
Ctrl+Scroll down
Ctrl+Scroll up

About

About RyuseiCode

RyuseiCode is a lightweight, extensible and accessible code editor, written in TypeScript. Basically, this is created for desktop evergreen browsers, but works on mobile devices. It also works on IE11 without any polyfills.💪

The RyuseiCode does not aim for a super robust code editor like Monaco Editor, but for good balance with simplicity and usability.

This library uses RyuseiLight as a syntax highlighter that is also written by me. Ryusei(流星, 流=flow, 星=star) is my personal project code, that means a shooting star in Japanese. 💫

Caveats

  • In IE, scrolling performance is not better. I'm reluctant to improve it because I don't believe there is no programmer who edits code in IE anymore.
  • The editor is not intended to run gigantic code. You will notice the input latency around 20,000 lines.