text: /**
   * Copyright (c) 2013-present, Facebook, Inc.
   * All rights reserved.
   *
   * This source code is licensed under the BSD-style license found in the
   * LICENSE file in the root directory of this source tree. An additional grant
   * of patent rights can be found in the PATENTS file in the same directory.
   *
   * @providesModule RichTextEditorUtil
   * @typechecks
   * 
   */
  
  'use strict';
  
  var DraftModifier = require('./DraftModifier');
  var EditorState = require('./EditorState');
  var SelectionState = require('./SelectionState');
  
  var adjustBlockDepthForContentState = require('./adjustBlockDepthForContentState');
  var nullthrows = require('fbjs/lib/nullthrows');
  
  var RichTextEditorUtil = {
    currentBlockContainsLink: function currentBlockContainsLink(editorState) {
      var selection = editorState.getSelection();
      var contentState = editorState.getCurrentContent();
      var entityMap = contentState.getEntityMap();
      return contentState.getBlockForKey(selection.getAnchorKey()).getCharacterList().slice(selection.getStartOffset(), selection.getEndOffset()).some(function (v) {
        var entity = v.getEntity();
        return !!entity && entityMap.__get(entity).getType() === 'LINK';
      });
    },
  
    getCurrentBlockType: function getCurrentBlockType(editorState) {
      var selection = editorState.getSelection();
      return editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType();
    },
  
    getDataObjectForLinkURL: function getDataObjectForLinkURL(uri) {
      return { url: uri.toString() };
    },
  
    handleKeyCommand: function handleKeyCommand(editorState, command) {
      switch (command) {
        case 'bold':
          return RichTextEditorUtil.toggleInlineStyle(editorState, 'BOLD');
        case 'italic':
          return RichTextEditorUtil.toggleInlineStyle(editorState, 'ITALIC');
        case 'underline':
          return RichTextEditorUtil.toggleInlineStyle(editorState, 'UNDERLINE');
        case 'code':
          return RichTextEditorUtil.toggleCode(editorState);
        case 'backspace':
        case 'backspace-word':
        case 'backspace-to-start-of-line':
          return RichTextEditorUtil.onBackspace(editorState);
        case 'delete':
        case 'delete-word':
        case 'delete-to-end-of-block':
          return RichTextEditorUtil.onDelete(editorState);
        default:
          // they may have custom editor commands; ignore those
          return null;
      }
    },
  
    insertSoftNewline: function insertSoftNewline(editorState) {
      var contentState = DraftModifier.insertText(editorState.getCurrentContent(), editorState.getSelection(), '\n', editorState.getCurrentInlineStyle(), null);
  
      var newEditorState = EditorState.push(editorState, contentState, 'insert-characters');
  
      return EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
    },
  
    /**
     * For collapsed selections at the start of styled blocks, backspace should
     * just remove the existing style.
     */
    onBackspace: function onBackspace(editorState) {
      var selection = editorState.getSelection();
      if (!selection.isCollapsed() || selection.getAnchorOffset() || selection.getFocusOffset()) {
        return null;
      }
  
      // First, try to remove a preceding atomic block.
      var content = editorState.getCurrentContent();
      var startKey = selection.getStartKey();
      var blockBefore = content.getBlockBefore(startKey);
  
      if (blockBefore && blockBefore.getType() === 'atomic') {
        var blockMap = content.getBlockMap()['delete'](blockBefore.getKey());
        var withoutAtomicBlock = content.merge({ blockMap: blockMap, selectionAfter: selection });
        if (withoutAtomicBlock !== content) {
          return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
        }
      }
  
      // If that doesn't succeed, try to remove the current block style.
      var withoutBlockStyle = RichTextEditorUtil.tryToRemoveBlockStyle(editorState);
  
      if (withoutBlockStyle) {
        return EditorState.push(editorState, withoutBlockStyle, 'change-block-type');
      }
  
      return null;
    },
  
    onDelete: function onDelete(editorState) {
      var selection = editorState.getSelection();
      if (!selection.isCollapsed()) {
        return null;
      }
  
      var content = editorState.getCurrentContent();
      var startKey = selection.getStartKey();
      var block = content.getBlockForKey(startKey);
      var length = block.getLength();
  
      // The cursor is somewhere within the text. Behave normally.
      if (selection.getStartOffset() < length) {
        return null;
      }
  
      var blockAfter = content.getBlockAfter(startKey);
  
      if (!blockAfter || blockAfter.getType() !== 'atomic') {
        return null;
      }
  
      var atomicBlockTarget = selection.merge({
        focusKey: blockAfter.getKey(),
        focusOffset: blockAfter.getLength()
      });
  
      var withoutAtomicBlock = DraftModifier.removeRange(content, atomicBlockTarget, 'forward');
  
      if (withoutAtomicBlock !== content) {
        return EditorState.push(editorState, withoutAtomicBlock, 'remove-range');
      }
  
      return null;
    },
  
    onTab: function onTab(event, editorState, maxDepth) {
      var selection = editorState.getSelection();
      var key = selection.getAnchorKey();
      if (key !== selection.getFocusKey()) {
        return editorState;
      }
  
      var content = editorState.getCurrentContent();
      var block = content.getBlockForKey(key);
      var type = block.getType();
      if (type !== 'unordered-list-item' && type !== 'ordered-list-item') {
        return editorState;
      }
  
      event.preventDefault();
  
      // Only allow indenting one level beyond the block above, and only if
      // the block above is a list item as well.
      var blockAbove = content.getBlockBefore(key);
      if (!blockAbove) {
        return editorState;
      }
  
      var typeAbove = blockAbove.getType();
      if (typeAbove !== 'unordered-list-item' && typeAbove !== 'ordered-list-item') {
        return editorState;
      }
  
      var depth = block.getDepth();
      if (!event.shiftKey && depth === maxDepth) {
        return editorState;
      }
  
      maxDepth = Math.min(blockAbove.getDepth() + 1, maxDepth);
  
      var withAdjustment = adjustBlockDepthForContentState(content, selection, event.shiftKey ? -1 : 1, maxDepth);
  
      return EditorState.push(editorState, withAdjustment, 'adjust-depth');
    },
  
    toggleBlockType: function toggleBlockType(editorState, blockType) {
      var selection = editorState.getSelection();
      var startKey = selection.getStartKey();
      var endKey = selection.getEndKey();
      var content = editorState.getCurrentContent();
      var target = selection;
  
      // Triple-click can lead to a selection that includes offset 0 of the
      // following block. The `SelectionState` for this case is accurate, but
      // we should avoid toggling block type for the trailing block because it
      // is a confusing interaction.
      if (startKey !== endKey && selection.getEndOffset() === 0) {
        var blockBefore = nullthrows(content.getBlockBefore(endKey));
        endKey = blockBefore.getKey();
        target = target.merge({
          anchorKey: startKey,
          anchorOffset: selection.getStartOffset(),
          focusKey: endKey,
          focusOffset: blockBefore.getLength(),
          isBackward: false
        });
      }
  
      var hasAtomicBlock = content.getBlockMap().skipWhile(function (_, k) {
        return k !== startKey;
      }).reverse().skipWhile(function (_, k) {
        return k !== endKey;
      }).some(function (v) {
        return v.getType() === 'atomic';
      });
  
      if (hasAtomicBlock) {
        return editorState;
      }
  
      var typeToSet = content.getBlockForKey(startKey).getType() === blockType ? 'unstyled' : blockType;
  
      return EditorState.push(editorState, DraftModifier.setBlockType(content, target, typeToSet), 'change-block-type');
    },
  
    toggleCode: function toggleCode(editorState) {
      var selection = editorState.getSelection();
      var anchorKey = selection.getAnchorKey();
      var focusKey = selection.getFocusKey();
  
      if (selection.isCollapsed() || anchorKey !== focusKey) {
        return RichTextEditorUtil.toggleBlockType(editorState, 'code-block');
      }
  
      return RichTextEditorUtil.toggleInlineStyle(editorState, 'CODE');
    },
  
    /**
     * Toggle the specified inline style for the selection. If the
     * user's selection is collapsed, apply or remove the style for the
     * internal state. If it is not collapsed, apply the change directly
     * to the document state.
     */
    toggleInlineStyle: function toggleInlineStyle(editorState, inlineStyle) {
      var selection = editorState.getSelection();
      var currentStyle = editorState.getCurrentInlineStyle();
  
      // If the selection is collapsed, toggle the specified style on or off and
      // set the result as the new inline style override. This will then be
      // used as the inline style for the next character to be inserted.
      if (selection.isCollapsed()) {
        return EditorState.setInlineStyleOverride(editorState, currentStyle.has(inlineStyle) ? currentStyle.remove(inlineStyle) : currentStyle.add(inlineStyle));
      }
  
      // If characters are selected, immediately apply or remove the
      // inline style on the document state itself.
      var content = editorState.getCurrentContent();
      var newContent;
  
      // If the style is already present for the selection range, remove it.
      // Otherwise, apply it.
      if (currentStyle.has(inlineStyle)) {
        newContent = DraftModifier.removeInlineStyle(content, selection, inlineStyle);
      } else {
        newContent = DraftModifier.applyInlineStyle(content, selection, inlineStyle);
      }
  
      return EditorState.push(editorState, newContent, 'change-inline-style');
    },
  
    toggleLink: function toggleLink(editorState, targetSelection, entityKey) {
      var withoutLink = DraftModifier.applyEntity(editorState.getCurrentContent(), targetSelection, entityKey);
  
      return EditorState.push(editorState, withoutLink, 'apply-entity');
    },
  
    /**
     * When a collapsed cursor is at the start of the first styled block, or 
     * an empty styled block, changes block to 'unstyled'. Returns null if 
     * block or selection does not meet that criteria.
     */
    tryToRemoveBlockStyle: function tryToRemoveBlockStyle(editorState) {
      var selection = editorState.getSelection();
      var offset = selection.getAnchorOffset();
      if (selection.isCollapsed() && offset === 0) {
        var key = selection.getAnchorKey();
        var content = editorState.getCurrentContent();
        var block = content.getBlockForKey(key);
  
        var firstBlock = content.getFirstBlock();
        if (block.getLength() > 0 && block !== firstBlock) {
          return null;
        }
  
        var type = block.getType();
        var blockBefore = content.getBlockBefore(key);
        if (type === 'code-block' && blockBefore && blockBefore.getType() === 'code-block') {
          return null;
        }
  
        if (type !== 'unstyled') {
          return DraftModifier.setBlockType(content, selection, 'unstyled');
        }
      }
      return null;
    }
  };
  
  module.exports = RichTextEditorUtil;