Source: utils/cursor.js

import {
  clearSelection,
  comparePosition
} from '../utils/selection-utils';
import { containsNode } from '../utils/dom-utils';
import Position from './cursor/position';
import Range from './cursor/range';
import { DIRECTION } from '../utils/key';

export { Position, Range };

const Cursor = class Cursor {
  constructor(editor) {
    this.editor = editor;
    this.renderTree = editor._renderTree;
    this.post = editor.post;
  }

  clearSelection() {
    clearSelection();
  }

  /**
   * @return {Boolean} true when there is either a collapsed cursor in the
   * editor's element or a selection that is contained in the editor's element
   */
  hasCursor() {
    return this.editor.hasRendered &&
           (this._hasCollapsedSelection() || this._hasSelection());
  }

  hasSelection() {
    return this.editor.hasRendered &&
           this._hasSelection();
  }

  /**
   * @return {Boolean} Can the cursor be on this element?
   */
  isAddressable(element) {
    let { renderTree } = this;
    let renderNode = renderTree.findRenderNodeFromElement(element);
    if (renderNode && renderNode.postNode.isCardSection) {
      let renderedElement = renderNode.element;

      // card sections have addressable text nodes containing ‌
      // as their first and last child
      if (element !== renderedElement &&
          element !== renderedElement.firstChild &&
          element !== renderedElement.lastChild) {
        return false;
      }
    }

    return !!renderNode;
  }

  /*
   * @return {Range} Cursor#Range object
   */
  get offsets() {
    if (!this.hasCursor()) { return Range.blankRange(); }

    const { selection, renderTree } = this;

    const {
      headNode, headOffset, tailNode, tailOffset, direction
    } = comparePosition(selection);

    const headPosition = Position.fromNode(renderTree, headNode, headOffset);
    const tailPosition = Position.fromNode(renderTree, tailNode, tailOffset);

    return new Range(headPosition, tailPosition, direction);
  }

  _findNodeForPosition(position) {
    let { section } = position;
    let node, offset;
    if (section.isCardSection) {
      offset = 0;
      if (position.offset === 0) {
        node = section.renderNode.element.firstChild;
      } else {
        node = section.renderNode.element.lastChild;
      }
    } else if (section.isBlank) {
      node = section.renderNode.cursorElement;
      offset = 0;
    } else {
      let {marker, offsetInMarker} = position;
      if (marker.isAtom) {
        if (offsetInMarker > 0) {
          // FIXME -- if there is a next marker, focus on it?
          offset = 0;
          node = marker.renderNode.tailTextNode;
        } else {
          offset = 0;
          node = marker.renderNode.headTextNode;
        }
      } else {
        node = marker.renderNode.element;
        offset = offsetInMarker;
      }
    }

    return {node, offset};
  }

  selectRange(range) {
    if (range.isBlank) {
      this.clearSelection();
      return;
    }

    const { head, tail, direction } = range;
    const { node:headNode, offset:headOffset } = this._findNodeForPosition(head),
          { node:tailNode, offset:tailOffset } = this._findNodeForPosition(tail);
    this._moveToNode(headNode, headOffset, tailNode, tailOffset, direction);
  }

  get selection() {
    return window.getSelection();
  }

  selectedText() {
    // FIXME remove this
    return this.selection.toString();
  }

  /**
   * @param {textNode} node
   * @param {integer} offset
   * @param {textNode} endNode
   * @param {integer} endOffset
   * @param {integer} direction forward or backward, default forward
   * @private
   */
  _moveToNode(node, offset, endNode, endOffset, direction=DIRECTION.FORWARD) {
    this.clearSelection();

    if (direction === DIRECTION.BACKWARD) {
      [node, offset, endNode, endOffset] = [ endNode, endOffset, node, offset ];
    }

    const range = document.createRange();
    range.setStart(node, offset);
    if (direction === DIRECTION.BACKWARD && !!this.selection.extend) {
      this.selection.addRange(range);
      this.selection.extend(endNode, endOffset);
    } else {
      range.setEnd(endNode, endOffset);
      this.selection.addRange(range);
    }
  }

  _hasSelection() {
    const element = this.editor.element;
    const { _selectionRange } = this;
    if (!_selectionRange || _selectionRange.collapsed) { return false; }

    return containsNode(element, this.selection.anchorNode) &&
           containsNode(element, this.selection.focusNode);
  }

  _hasCollapsedSelection() {
    const { _selectionRange } = this;
    if (!_selectionRange) { return false; }

    const element = this.editor.element;
    return containsNode(element, this.selection.anchorNode);
  }

  get _selectionRange() {
    const { selection } = this;
    if (selection.rangeCount === 0) { return null; }
    return selection.getRangeAt(0);
  }
};

export default Cursor;