Source: editor/edit-state.js

import { contains, isArrayEqual } from 'mobiledoc-kit/utils/array-utils';

/**
 * Used by {@link Editor} to manage its current state (cursor, active markups
 * and active sections).
 * @private
 */
class EditState {
  constructor(editor) {
    this.editor = editor;
    this.prevState = this.state = this._readState();
  }

  /**
   * Cache the last state, force a reread of current state
   */
  reset() {
    this.prevState = this.state;
    this.state = this._readState();
  }

  /**
   * @return {Boolean} Whether the input mode (active markups or active section tag names)
   * has changed.
   */
  inputModeDidChange() {
    let { state, prevState } = this;
    return (!isArrayEqual(state.activeMarkups, prevState.activeMarkups) ||
            !isArrayEqual(state.activeSectionTagNames, prevState.activeSectionTagNames));
  }

  /**
   * @return {Boolean} Whether the range has changed.
   */
  rangeDidChange() {
    let { state, prevState } = this;
    return !state.range.isEqual(prevState.range);
  }

  /**
   * @return {Range}
   */
  get range() {
    return this.state.range;
  }

  /**
   * @return {Section[]}
   */
  get activeSections() {
    return this.state.activeSections;
  }

  /**
   * @return {Markup[]}
   */
  get activeMarkups() {
    return this.state.activeMarkups;
  }

  /**
   * Update the editor's markup state. This is used when, e.g.,
   * a user types meta+B when the editor has a cursor but no selected text;
   * in this case the editor needs to track that it has an active "b" markup
   * and apply it to the next text the user types.
   */
  toggleMarkupState(markup) {
    if (contains(this.activeMarkups, markup)) {
      this._removeActiveMarkup(markup);
    } else {
      this._addActiveMarkup(markup);
    }
  }

  _readState() {
    let range = this._readRange();
    let state = {
      range:          range,
      activeMarkups:  this._readActiveMarkups(range),
      activeSections: this._readActiveSections(range)
    };
    // Section objects are 'live', so to check that they changed, we
    // need to map their tagNames now (and compare to mapped tagNames later).
    // In addition, to catch changes from ul -> ol, we keep track of the
    // un-nested tag names (otherwise we'd only see li -> li change)
    state.activeSectionTagNames = state.activeSections.map(s => {
      return s.isNested ? s.parent.tagName : s.tagName;
    });
    return state;
  }

  _readRange() {
    return this.editor.range;
  }

  _readActiveSections(range) {
    let { head, tail } = range;
    let { editor: { post } } = this;
    if (range.isBlank) {
      return [];
    } else {
      return post.sections.readRange(head.section, tail.section);
    }
  }

  _readActiveMarkups(range) {
    let { editor: { post } } = this;
    return post.markupsInRange(range);
  }

  _removeActiveMarkup(markup) {
    let index = this.state.activeMarkups.indexOf(markup);
    this.state.activeMarkups.splice(index, 1);
  }

  _addActiveMarkup(markup) {
    this.state.activeMarkups.push(markup);
  }
}

export default EditState;