Source: editor/edit-state.js

import {
  contains,
  isArrayEqual,
  objectToSortedKVArray
} from 'mobiledoc-kit/utils/array-utils';
import Range from 'mobiledoc-kit/utils/cursor/range';

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

    let defaultState = {
      range: Range.blankRange(),
      activeMarkups: [],
      activeSections: [],
      activeSectionTagNames: [],
      activeSectionAttributes: {}
    };

    this.prevState = this.state = defaultState;
  }

  updateRange(newRange) {
    this.prevState = this.state;
    this.state = this._readState(newRange);
  }

  destroy() {
    this.editor = null;
    this.prevState = this.state = null;
  }

  /**
   * @return {Boolean}
   */
  rangeDidChange() {
    let { state: { range } , prevState: {range: prevRange} } = this;

    return !prevRange.isEqual(range);
  }

  /**
   * @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) ||
            !isArrayEqual(objectToSortedKVArray(state.activeSectionAttributes), objectToSortedKVArray(prevState.activeSectionAttributes)));
  }

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

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


  /**
   * @return {Object}
   */
  get activeSectionAttributes() {
    return this.state.activeSectionAttributes;
  }

  /**
   * @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(range) {
    let state = {
      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;
    });
    state.activeSectionAttributes = this._readSectionAttributes(state.activeSections);
    return state;
  }

  _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);
  }

  _readSectionAttributes(sections) {
    return sections.reduce((sectionAttributes, s) => {
      let attributes = s.isNested ? s.parent.attributes : s.attributes;
      Object.keys(attributes || {}).forEach(attrName => {
        let camelizedAttrName = attrName.replace(/^data-md-/, '');
        let attrValue = attributes[attrName];
        sectionAttributes[camelizedAttrName] = sectionAttributes[camelizedAttrName] || [];
        if (!contains(sectionAttributes[camelizedAttrName], attrValue)) {
          sectionAttributes[camelizedAttrName].push(attrValue);
        }
      });
      return sectionAttributes;
    }, {});
  }

  _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;