chromium/chrome/browser/resources/side_panel/bookmarks/power_bookmarks_drag_manager.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../strings.m.js';

import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';

import {PowerBookmarkRowElement} from './power_bookmark_row.js';

const ROOT_FOLDER_ID = '0';

export const DROP_POSITION_ATTR = 'drop-position';

export enum DropPosition {
  INTO = 'into',
}

interface PowerBookmarksDragDelegate extends HTMLElement {
  getFallbackBookmark(): chrome.bookmarks.BookmarkTreeNode;
  getFallbackDropTargetElement(): HTMLElement;
  onFinishDrop(dropTarget: chrome.bookmarks.BookmarkTreeNode): void;
}

class DragSession {
  private delegate_: PowerBookmarksDragDelegate;
  private dragData_: chrome.bookmarkManagerPrivate.DragData;
  private lastDragOverElement_: PowerBookmarkRowElement|null = null;
  private lastDropTargetBookmark_: chrome.bookmarks.BookmarkTreeNode|null =
      null;
  private lastPointerWasTouch_ = false;

  constructor(
      delegate: PowerBookmarksDragDelegate,
      dragData: chrome.bookmarkManagerPrivate.DragData) {
    this.delegate_ = delegate;
    this.dragData_ = dragData;
  }

  start(e: DragEvent) {
    chrome.bookmarkManagerPrivate.startDrag(
        this.dragData_.elements!.map(bookmark => bookmark.id), 0,
        this.lastPointerWasTouch_, e.clientX, e.clientY);
  }

  update(e: DragEvent) {
    const dragOverElement = e.composedPath().find(target => {
      return target instanceof PowerBookmarkRowElement;
    }) as PowerBookmarkRowElement;

    if (!dragOverElement) {
      // Invalid drag over element. Cancel session.
      this.cancel();
      return;
    } else if (dragOverElement === this.lastDragOverElement_) {
      // State has not changed, nothing to update.
      return;
    }

    this.resetState_();

    const dragOverBookmark = dragOverElement.bookmark;
    let dropTargetBookmark = dragOverBookmark;

    const invalidDropTarget = dropTargetBookmark.unmodifiable ||
        dropTargetBookmark.url ||
        (this.dragData_.elements &&
         this.dragData_.elements.some(
             element => element.id === dropTargetBookmark.id));
    if (invalidDropTarget) {
      dropTargetBookmark = this.delegate_.getFallbackBookmark();
    }

    const draggedBookmarks = this.dragData_.elements!;
    let dropTargetIsParent = true;
    draggedBookmarks.forEach((bookmark: chrome.bookmarks.BookmarkTreeNode) => {
      if (bookmark.parentId !== dropTargetBookmark.id) {
        dropTargetIsParent = false;
      }
    });
    if (draggedBookmarks.length === 0 || dropTargetIsParent) {
      this.cancel();
      return;
    }

    if (dragOverBookmark.url) {
      this.delegate_.getFallbackDropTargetElement().setAttribute(
          DROP_POSITION_ATTR, DropPosition.INTO);
    } else {
      dragOverElement.setAttribute(DROP_POSITION_ATTR, DropPosition.INTO);
    }
    this.lastDragOverElement_ = dragOverElement;
    this.lastDropTargetBookmark_ = dropTargetBookmark;
  }

  cancel() {
    this.resetState_();
    this.lastDragOverElement_ = null;
    this.lastDropTargetBookmark_ = null;
  }

  finish() {
    // TODO(crbug.com/40267573): Ensure it is possible to drag bookmarks into an
    // empty active folder.
    if (!this.lastDropTargetBookmark_) {
      return;
    }
    chrome.bookmarkManagerPrivate
        .drop(this.lastDropTargetBookmark_.id, /* index */ undefined)
        .then(() => {
          this.delegate_.onFinishDrop(this.lastDropTargetBookmark_!);
          this.cancel();
        });
  }

  private resetState_() {
    if (this.lastDragOverElement_) {
      this.lastDragOverElement_.removeAttribute(DROP_POSITION_ATTR);
    }
    this.delegate_.getFallbackDropTargetElement().removeAttribute(
        DROP_POSITION_ATTR);
  }

  static createFromBookmark(
      delegate: PowerBookmarksDragDelegate,
      bookmark: chrome.bookmarks.BookmarkTreeNode) {
    return new DragSession(delegate, {
      elements: [bookmark],
      sameProfile: true,
    });
  }
}

export class PowerBookmarksDragManager {
  private delegate_: PowerBookmarksDragDelegate;
  private dragSession_: DragSession|null;
  private eventTracker_: EventTracker = new EventTracker();

  constructor(delegate: PowerBookmarksDragDelegate) {
    this.delegate_ = delegate;
  }

  startObserving() {
    this.eventTracker_.removeAll();
    this.eventTracker_.add(
        this.delegate_, 'dragstart',
        (e: Event) => this.onDragStart_(e as DragEvent));
    this.eventTracker_.add(
        this.delegate_, 'dragover',
        (e: Event) => this.onDragOver_(e as DragEvent));
    this.eventTracker_.add(
        this.delegate_, 'dragleave', () => this.onDragLeave_());
    this.eventTracker_.add(this.delegate_, 'dragend', () => this.cancelDrag_());
    this.eventTracker_.add(
        this.delegate_, 'drop', (e: Event) => this.onDrop_(e as DragEvent));

    if (loadTimeData.getBoolean('editBookmarksEnabled')) {
      chrome.bookmarkManagerPrivate.onDragEnter.addListener(
          (dragData: chrome.bookmarkManagerPrivate.DragData) =>
              this.onChromeDragEnter_(dragData));
      chrome.bookmarkManagerPrivate.onDragLeave.addListener(
          () => this.cancelDrag_());
    }
  }

  stopObserving() {
    this.eventTracker_.removeAll();
  }

  private cancelDrag_() {
    if (!this.dragSession_) {
      return;
    }
    this.dragSession_.cancel();
    this.dragSession_ = null;
  }

  private onChromeDragEnter_(dragData: chrome.bookmarkManagerPrivate.DragData) {
    if (this.dragSession_) {
      // A drag session is already in flight.
      return;
    }

    this.dragSession_ = new DragSession(this.delegate_, dragData);
  }

  private onDragStart_(e: DragEvent) {
    e.preventDefault();
    if (!loadTimeData.getBoolean('editBookmarksEnabled')) {
      return;
    }

    const bookmark =
        (e.composedPath().find(target => (target as HTMLElement).draggable) as
         PowerBookmarkRowElement)
            .bookmark;
    if (!bookmark ||
        /* Cannot drag root's children. */ bookmark.parentId ===
            ROOT_FOLDER_ID ||
        bookmark.unmodifiable) {
      return;
    }

    this.dragSession_ =
        DragSession.createFromBookmark(this.delegate_, bookmark);
    this.dragSession_.start(e);
  }

  private onDragOver_(e: DragEvent) {
    e.preventDefault();
    if (!this.dragSession_) {
      return;
    }
    this.dragSession_.update(e);
  }

  private onDragLeave_() {
    if (!this.dragSession_) {
      return;
    }

    this.dragSession_.cancel();
  }

  private onDrop_(e: DragEvent) {
    if (!this.dragSession_) {
      return;
    }

    e.preventDefault();
    this.dragSession_.finish();
    this.dragSession_ = null;
  }
}