text: // Copyright 2013 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  cr.define('dnd', function() {
    'use strict';
  
    /** @const */ var BookmarkList = bmm.BookmarkList;
    /** @const */ var ListItem = cr.ui.ListItem;
    /** @const */ var TreeItem = cr.ui.TreeItem;
  
    /**
     * Enumeration of valid drop locations relative to an element. These are
     * bit masks to allow combining multiple locations in a single value.
     * @enum {number}
     * @const
     */
    var DropPosition = {
      NONE: 0,
      ABOVE: 1,
      ON: 2,
      BELOW: 4
    };
  
    /**
     * @type {Object} Drop information calculated in |handleDragOver|.
     */
    var dropDestination = null;
  
    /**
      * @type {number} Timer id used to help minimize flicker.
      */
    var removeDropIndicatorTimer;
  
    /**
      * The element currently targeted by a touch.
      * @type {Element}
      */
    var currentTouchTarget;
  
    /**
      * The element that had a style applied it to indicate the drop location.
      * This is used to easily remove the style when necessary.
      * @type {Element}
      */
    var lastIndicatorElement;
  
    /**
      * The style that was applied to indicate the drop location.
      * @type {?string}
      */
    var lastIndicatorClassName;
  
    var dropIndicator = {
      /**
       * Applies the drop indicator style on the target element and stores that
       * information to easily remove the style in the future.
       */
      addDropIndicatorStyle: function(indicatorElement, position) {
        var indicatorStyleName = position == DropPosition.ABOVE ? 'drag-above' :
                                 position == DropPosition.BELOW ? 'drag-below' :
                                 'drag-on';
  
        lastIndicatorElement = indicatorElement;
        lastIndicatorClassName = indicatorStyleName;
  
        indicatorElement.classList.add(indicatorStyleName);
      },
  
      /**
       * Clears the drop indicator style from the last element was the drop target
       * so the drop indicator is no longer for that element.
       */
      removeDropIndicatorStyle: function() {
        if (!lastIndicatorElement || !lastIndicatorClassName)
          return;
        lastIndicatorElement.classList.remove(lastIndicatorClassName);
        lastIndicatorElement = null;
        lastIndicatorClassName = null;
      },
  
      /**
        * Displays the drop indicator on the current drop target to give the
        * user feedback on where the drop will occur.
        */
      update: function(dropDest) {
        window.clearTimeout(removeDropIndicatorTimer);
  
        var indicatorElement = dropDest.element;
        var position = dropDest.position;
        if (dropDest.element instanceof BookmarkList) {
          // For an empty bookmark list use 'drop-above' style.
          position = DropPosition.ABOVE;
        } else if (dropDest.element instanceof TreeItem) {
          indicatorElement = indicatorElement.querySelector('.tree-row');
        }
        dropIndicator.removeDropIndicatorStyle();
        dropIndicator.addDropIndicatorStyle(indicatorElement, position);
      },
  
      /**
       * Stop displaying the drop indicator.
       */
      finish: function() {
        // The use of a timeout is in order to reduce flickering as we move
        // between valid drop targets.
        window.clearTimeout(removeDropIndicatorTimer);
        removeDropIndicatorTimer = window.setTimeout(function() {
          dropIndicator.removeDropIndicatorStyle();
        }, 100);
      }
    };
  
    /**
      * Delay for expanding folder when pointer hovers on folder in tree view in
      * milliseconds.
      * @type {number}
      * @const
      */
    // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is
    // taken from Windows default settings.
    var EXPAND_FOLDER_DELAY = 400;
  
    /**
      * The timestamp when the mouse was over a folder during a drag operation.
      * Used to open the hovered folder after a certain time.
      * @type {number}
      */
    var lastHoverOnFolderTimeStamp = 0;
  
    /**
      * Expand a folder if the user has hovered for longer than the specified
      * time during a drag action.
      */
    function updateAutoExpander(eventTimeStamp, overElement) {
      // Expands a folder in tree view when pointer hovers on it longer than
      // EXPAND_FOLDER_DELAY.
      var hoverOnFolderTimeStamp = lastHoverOnFolderTimeStamp;
      lastHoverOnFolderTimeStamp = 0;
      if (hoverOnFolderTimeStamp) {
        if (eventTimeStamp - hoverOnFolderTimeStamp >= EXPAND_FOLDER_DELAY)
          overElement.expanded = true;
        else
          lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp;
      } else if (overElement instanceof TreeItem &&
                  bmm.isFolder(overElement.bookmarkNode) &&
                  overElement.hasChildren &&
                  !overElement.expanded) {
        lastHoverOnFolderTimeStamp = eventTimeStamp;
      }
    }
  
    /**
      * Stores the information about the bookmark and folders being dragged.
      * @type {Object}
      */
    var dragData = null;
    var dragInfo = {
      handleChromeDragEnter: function(newDragData) {
        dragData = newDragData;
      },
      clearDragData: function() {
        dragData = null;
      },
      isDragValid: function() {
        return !!dragData;
      },
      isSameProfile: function() {
        return dragData && dragData.sameProfile;
      },
      isDraggingFolders: function() {
        return dragData && dragData.elements.some(function(node) {
          return !node.url;
        });
      },
      isDraggingBookmark: function(bookmarkId) {
        return dragData && dragData.elements.some(function(node) {
          return node.id == bookmarkId;
        });
      },
      isDraggingChildBookmark: function(folderId) {
        return dragData && dragData.elements.some(function(node) {
          return node.parentId == folderId;
        });
      },
      isDraggingFolderToDescendant: function(bookmarkNode) {
        return dragData && dragData.elements.some(function(node) {
          var dragFolder = bmm.treeLookup[node.id];
          var dragFolderNode = dragFolder && dragFolder.bookmarkNode;
          return dragFolderNode && bmm.contains(dragFolderNode, bookmarkNode);
        });
      }
    };
  
    /**
     * External function to select folders or bookmarks after a drop action.
     * @type {?Function}
     */
    var selectItemsAfterUserAction = null;
  
    function getBookmarkElement(el) {
      while (el && !el.bookmarkNode) {
        el = el.parentNode;
      }
      return el;
    }
  
    // If we are over the list and the list is showing search result, we cannot
    // drop.
    function isOverSearch(overElement) {
      return bmm.list.isSearch() && bmm.list.contains(overElement);
    }
  
    /**
     * Determines the valid drop positions for the given target element.
     * @param {!HTMLElement} overElement The element that we are currently
     *     dragging over.
     * @return {DropPosition} An bit field enumeration of valid drop locations.
     */
    function calculateValidDropTargets(overElement) {
      // Don't allow dropping if there is an ephemeral item being edited.
      if (bmm.list.hasEphemeral())
        return DropPosition.NONE;
  
      if (!dragInfo.isDragValid() || isOverSearch(overElement))
        return DropPosition.NONE;
  
      if (dragInfo.isSameProfile() &&
          (dragInfo.isDraggingBookmark(overElement.bookmarkNode.id) ||
           dragInfo.isDraggingFolderToDescendant(overElement.bookmarkNode))) {
        return DropPosition.NONE;
      }
  
      var canDropInfo = calculateDropAboveBelow(overElement);
      if (canDropOn(overElement))
        canDropInfo |= DropPosition.ON;
  
      return canDropInfo;
    }
  
    function calculateDropAboveBelow(overElement) {
      if (overElement instanceof BookmarkList)
        return DropPosition.NONE;
  
      // We cannot drop between Bookmarks bar and Other bookmarks.
      if (overElement.bookmarkNode.parentId == bmm.ROOT_ID)
        return DropPosition.NONE;
  
      var isOverTreeItem = overElement instanceof TreeItem;
      var isOverExpandedTree = isOverTreeItem && overElement.expanded;
      var isDraggingFolders = dragInfo.isDraggingFolders();
  
      // We can only drop between items in the tree if we have any folders.
      if (isOverTreeItem && !isDraggingFolders)
        return DropPosition.NONE;
  
      // When dragging from a different profile we do not need to consider
      // conflicts between the dragged items and the drop target.
      if (!dragInfo.isSameProfile()) {
        // Don't allow dropping below an expanded tree item since it is confusing
        // to the user anyway.
        return isOverExpandedTree ? DropPosition.ABOVE :
                                    (DropPosition.ABOVE | DropPosition.BELOW);
      }
  
      var resultPositions = DropPosition.NONE;
  
      // Cannot drop above if the item above is already in the drag source.
      var previousElem = overElement.previousElementSibling;
      if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.bookmarkId))
        resultPositions |= DropPosition.ABOVE;
  
      // Don't allow dropping below an expanded tree item since it is confusing
      // to the user anyway.
      if (isOverExpandedTree)
        return resultPositions;
  
      // Cannot drop below if the item below is already in the drag source.
      var nextElement = overElement.nextElementSibling;
      if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.bookmarkId))
        resultPositions |= DropPosition.BELOW;
  
      return resultPositions;
    }
  
    /**
     * Determine whether we can drop the dragged items on the drop target.
     * @param {!HTMLElement} overElement The element that we are currently
     *     dragging over.
     * @return {boolean} Whether we can drop the dragged items on the drop
     *     target.
     */
    function canDropOn(overElement) {
      // We can only drop on a folder.
      if (!bmm.isFolder(overElement.bookmarkNode))
        return false;
  
      if (!dragInfo.isSameProfile())
        return true;
  
      if (overElement instanceof BookmarkList) {
        // We are trying to drop an item past the last item. This is
        // only allowed if dragged item is different from the last item
        // in the list.
        var listItems = bmm.list.items;
        var len = listItems.length;
        if (!len || !dragInfo.isDraggingBookmark(listItems[len - 1].bookmarkId))
          return true;
      }
  
      return !dragInfo.isDraggingChildBookmark(overElement.bookmarkNode.id);
    }
  
    /**
     * Callback for the dragstart event.
     * @param {Event} e The dragstart event.
     */
    function handleDragStart(e) {
      // Determine the selected bookmarks.
      var target = e.target;
      var draggedNodes = [];
      var isFromTouch = target == currentTouchTarget;
  
      if (target instanceof ListItem) {
        // Use selected items.
        draggedNodes = target.parentNode.selectedItems;
      } else if (target instanceof TreeItem) {
        draggedNodes.push(target.bookmarkNode);
      }
  
      // We manage starting the drag by using the extension API.
      e.preventDefault();
  
      // Do not allow dragging if there is an ephemeral item being edited at the
      // moment.
      if (bmm.list.hasEphemeral())
        return;
  
      if (draggedNodes.length) {
        // If we are dragging a single link, we can do the *Link* effect.
        // Otherwise, we only allow copy and move.
        e.dataTransfer.effectAllowed = draggedNodes.length == 1 &&
            !bmm.isFolder(draggedNodes[0]) ? 'copyMoveLink' : 'copyMove';
  
        chrome.bookmarkManagerPrivate.startDrag(draggedNodes.map(function(node) {
          return node.id;
        }), isFromTouch);
      }
    }
  
    function handleDragEnter(e) {
      e.preventDefault();
    }
  
    /**
     * Calback for the dragover event.
     * @param {Event} e The dragover event.
     */
    function handleDragOver(e) {
      // Allow DND on text inputs.
      if (e.target.tagName != 'INPUT') {
        // The default operation is to allow dropping links etc to do navigation.
        // We never want to do that for the bookmark manager.
        e.preventDefault();
  
        // Set to none. This will get set to something if we can do the drop.
        e.dataTransfer.dropEffect = 'none';
      }
  
      if (!dragInfo.isDragValid())
        return;
  
      var overElement = getBookmarkElement(e.target) ||
                        (e.target == bmm.list ? bmm.list : null);
      if (!overElement)
        return;
  
      updateAutoExpander(e.timeStamp, overElement);
  
      var canDropInfo = calculateValidDropTargets(overElement);
      if (canDropInfo == DropPosition.NONE)
        return;
  
      // Now we know that we can drop. Determine if we will drop above, on or
      // below based on mouse position etc.
  
      dropDestination = calcDropPosition(e.clientY, overElement, canDropInfo);
      if (!dropDestination) {
        e.dataTransfer.dropEffect = 'none';
        return;
      }
  
      e.dataTransfer.dropEffect = dragInfo.isSameProfile() ? 'move' : 'copy';
      dropIndicator.update(dropDestination);
    }
  
    /**
     * This function determines where the drop will occur relative to the element.
     * @return {?Object} If no valid drop position is found, null, otherwise
     *     an object containing the following parameters:
     *       element - The target element that will receive the drop.
     *       position - A |DropPosition| relative to the |element|.
     */
    function calcDropPosition(elementClientY, overElement, canDropInfo) {
      if (overElement instanceof BookmarkList) {
        // Dropping on the BookmarkList either means dropping below the last
        // bookmark element or on the list itself if it is empty.
        var length = overElement.items.length;
        if (length)
          return {
            element: overElement.getListItemByIndex(length - 1),
            position: DropPosition.BELOW
          };
        return {element: overElement, position: DropPosition.ON};
      }
  
      var above = canDropInfo & DropPosition.ABOVE;
      var below = canDropInfo & DropPosition.BELOW;
      var on = canDropInfo & DropPosition.ON;
      var rect = overElement.getBoundingClientRect();
      var yRatio = (elementClientY - rect.top) / rect.height;
  
      if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on)))
        return {element: overElement, position: DropPosition.ABOVE};
      if (below && (yRatio > .75 || yRatio > .5 && (!above || !on)))
        return {element: overElement, position: DropPosition.BELOW};
      if (on)
        return {element: overElement, position: DropPosition.ON};
      return null;
    }
  
    function calculateDropInfo(eventTarget, dropDestination) {
      if (!dropDestination || !dragInfo.isDragValid())
        return null;
  
      var dropPos = dropDestination.position;
      var relatedNode = dropDestination.element.bookmarkNode;
      var dropInfoResult = {
          selectTarget: null,
          selectedTreeId: -1,
          parentId: dropPos == DropPosition.ON ? relatedNode.id :
                                                 relatedNode.parentId,
          index: -1,
          relatedIndex: -1
        };
  
      // Try to find the index in the dataModel so we don't have to always keep
      // the index for the list items up to date.
      var overElement = getBookmarkElement(eventTarget);
      if (overElement instanceof ListItem) {
        dropInfoResult.relatedIndex =
            overElement.parentNode.dataModel.indexOf(relatedNode);
        dropInfoResult.selectTarget = bmm.list;
      } else if (overElement instanceof BookmarkList) {
        dropInfoResult.relatedIndex = overElement.dataModel.length - 1;
        dropInfoResult.selectTarget = bmm.list;
      } else {
        // Tree
        dropInfoResult.relatedIndex = relatedNode.index;
        dropInfoResult.selectTarget = bmm.tree;
        dropInfoResult.selectedTreeId =
            bmm.tree.selectedItem ? bmm.tree.selectedItem.bookmarkId : null;
      }
  
      if (dropPos == DropPosition.ABOVE)
        dropInfoResult.index = dropInfoResult.relatedIndex;
      else if (dropPos == DropPosition.BELOW)
        dropInfoResult.index = dropInfoResult.relatedIndex + 1;
  
      return dropInfoResult;
    }
  
    function handleDragLeave(e) {
      dropIndicator.finish();
    }
  
    function handleDrop(e) {
      var dropInfo = calculateDropInfo(e.target, dropDestination);
      if (dropInfo) {
        selectItemsAfterUserAction(dropInfo.selectTarget,
                                   dropInfo.selectedTreeId);
        if (dropInfo.index != -1)
          chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index);
        else
          chrome.bookmarkManagerPrivate.drop(dropInfo.parentId);
  
        e.preventDefault();
      }
      dropDestination = null;
      dropIndicator.finish();
    }
  
    function setCurrentTouchTarget(e) {
      // Only set a new target for a single touch point.
      if (e.touches.length == 1)
        currentTouchTarget = getBookmarkElement(e.target);
    }
  
    function clearCurrentTouchTarget(e) {
      if (getBookmarkElement(e.target) == currentTouchTarget)
        currentTouchTarget = null;
    }
  
    function clearDragData() {
      dragInfo.clearDragData();
      dropDestination = null;
    }
  
    function init(selectItemsAfterUserActionFunction) {
      function deferredClearData() {
        setTimeout(clearDragData, 0);
      }
  
      selectItemsAfterUserAction = selectItemsAfterUserActionFunction;
  
      document.addEventListener('dragstart', handleDragStart);
      document.addEventListener('dragenter', handleDragEnter);
      document.addEventListener('dragover', handleDragOver);
      document.addEventListener('dragleave', handleDragLeave);
      document.addEventListener('drop', handleDrop);
      document.addEventListener('dragend', deferredClearData);
      document.addEventListener('mouseup', deferredClearData);
      document.addEventListener('mousedown', clearCurrentTouchTarget);
      document.addEventListener('touchcancel', clearCurrentTouchTarget);
      document.addEventListener('touchend', clearCurrentTouchTarget);
      document.addEventListener('touchstart', setCurrentTouchTarget);
  
      chrome.bookmarkManagerPrivate.onDragEnter.addListener(
          dragInfo.handleChromeDragEnter);
      chrome.bookmarkManagerPrivate.onDragLeave.addListener(deferredClearData);
      chrome.bookmarkManagerPrivate.onDrop.addListener(deferredClearData);
    }
    return {init: init};
  });
  