| /**
 * CaretUtils.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
/**
 * Utility functions shared by the caret logic.
 *
 * @private
 * @class tinymce.caret.CaretUtils
 */
define(
  'tinymce.core.caret.CaretUtils',
  [
    "tinymce.core.util.Fun",
    "tinymce.core.dom.TreeWalker",
    "tinymce.core.dom.NodeType",
    "tinymce.core.caret.CaretPosition",
    "tinymce.core.caret.CaretContainer",
    "tinymce.core.caret.CaretCandidate"
  ],
  function (Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) {
    var isContentEditableTrue = NodeType.isContentEditableTrue,
      isContentEditableFalse = NodeType.isContentEditableFalse,
      isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption list-item'),
      isCaretContainer = CaretContainer.isCaretContainer,
      isCaretContainerBlock = CaretContainer.isCaretContainerBlock,
      curry = Fun.curry,
      isElement = NodeType.isElement,
      isCaretCandidate = CaretCandidate.isCaretCandidate;
    var isForwards = function (direction) {
      return direction > 0;
    };
    var isBackwards = function (direction) {
      return direction < 0;
    };
    var skipCaretContainers = function (walk, shallow) {
      var node;
      while ((node = walk(shallow))) {
        if (!isCaretContainerBlock(node)) {
          return node;
        }
      }
      return null;
    };
    var findNode = function (node, direction, predicateFn, rootNode, shallow) {
      var walker = new TreeWalker(node, rootNode);
      if (isBackwards(direction)) {
        if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
          node = skipCaretContainers(walker.prev, true);
          if (predicateFn(node)) {
            return node;
          }
        }
        while ((node = skipCaretContainers(walker.prev, shallow))) {
          if (predicateFn(node)) {
            return node;
          }
        }
      }
      if (isForwards(direction)) {
        if (isContentEditableFalse(node) || isCaretContainerBlock(node)) {
          node = skipCaretContainers(walker.next, true);
          if (predicateFn(node)) {
            return node;
          }
        }
        while ((node = skipCaretContainers(walker.next, shallow))) {
          if (predicateFn(node)) {
            return node;
          }
        }
      }
      return null;
    };
    var getEditingHost = function (node, rootNode) {
      for (node = node.parentNode; node && node != rootNode; node = node.parentNode) {
        if (isContentEditableTrue(node)) {
          return node;
        }
      }
      return rootNode;
    };
    var getParentBlock = function (node, rootNode) {
      while (node && node != rootNode) {
        if (isBlockLike(node)) {
          return node;
        }
        node = node.parentNode;
      }
      return null;
    };
    var isInSameBlock = function (caretPosition1, caretPosition2, rootNode) {
      return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode);
    };
    var isInSameEditingHost = function (caretPosition1, caretPosition2, rootNode) {
      return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode);
    };
    var getChildNodeAtRelativeOffset = function (relativeOffset, caretPosition) {
      var container, offset;
      if (!caretPosition) {
        return null;
      }
      container = caretPosition.container();
      offset = caretPosition.offset();
      if (!isElement(container)) {
        return null;
      }
      return container.childNodes[offset + relativeOffset];
    };
    var beforeAfter = function (before, node) {
      var range = node.ownerDocument.createRange();
      if (before) {
        range.setStartBefore(node);
        range.setEndBefore(node);
      } else {
        range.setStartAfter(node);
        range.setEndAfter(node);
      }
      return range;
    };
    var isNodesInSameBlock = function (rootNode, node1, node2) {
      return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode);
    };
    var lean = function (left, rootNode, node) {
      var sibling, siblingName;
      if (left) {
        siblingName = 'previousSibling';
      } else {
        siblingName = 'nextSibling';
      }
      while (node && node != rootNode) {
        sibling = node[siblingName];
        if (isCaretContainer(sibling)) {
          sibling = sibling[siblingName];
        }
        if (isContentEditableFalse(sibling)) {
          if (isNodesInSameBlock(rootNode, sibling, node)) {
            return sibling;
          }
          break;
        }
        if (isCaretCandidate(sibling)) {
          break;
        }
        node = node.parentNode;
      }
      return null;
    };
    var before = curry(beforeAfter, true);
    var after = curry(beforeAfter, false);
    var normalizeRange = function (direction, rootNode, range) {
      var node, container, offset, location;
      var leanLeft = curry(lean, true, rootNode);
      var leanRight = curry(lean, false, rootNode);
      container = range.startContainer;
      offset = range.startOffset;
      if (CaretContainer.isCaretContainerBlock(container)) {
        if (!isElement(container)) {
          container = container.parentNode;
        }
        location = container.getAttribute('data-mce-caret');
        if (location == 'before') {
          node = container.nextSibling;
          if (isContentEditableFalse(node)) {
            return before(node);
          }
        }
        if (location == 'after') {
          node = container.previousSibling;
          if (isContentEditableFalse(node)) {
            return after(node);
          }
        }
      }
      if (!range.collapsed) {
        return range;
      }
      if (NodeType.isText(container)) {
        if (isCaretContainer(container)) {
          if (direction === 1) {
            node = leanRight(container);
            if (node) {
              return before(node);
            }
            node = leanLeft(container);
            if (node) {
              return after(node);
            }
          }
          if (direction === -1) {
            node = leanLeft(container);
            if (node) {
              return after(node);
            }
            node = leanRight(container);
            if (node) {
              return before(node);
            }
          }
          return range;
        }
        if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) {
          if (direction === 1) {
            node = leanRight(container);
            if (node) {
              return before(node);
            }
          }
          return range;
        }
        if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) {
          if (direction === -1) {
            node = leanLeft(container);
            if (node) {
              return after(node);
            }
          }
          return range;
        }
        if (offset === container.data.length) {
          node = leanRight(container);
          if (node) {
            return before(node);
          }
          return range;
        }
        if (offset === 0) {
          node = leanLeft(container);
          if (node) {
            return after(node);
          }
          return range;
        }
      }
      return range;
    };
    var isNextToContentEditableFalse = function (relativeOffset, caretPosition) {
      return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition));
    };
    return {
      isForwards: isForwards,
      isBackwards: isBackwards,
      findNode: findNode,
      getEditingHost: getEditingHost,
      getParentBlock: getParentBlock,
      isInSameBlock: isInSameBlock,
      isInSameEditingHost: isInSameEditingHost,
      isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0),
      isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1),
      normalizeRange: normalizeRange
    };
  }
);
 |