mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 03:54:24 +01:00
tree: dnd bubble behavior
This commit is contained in:
@@ -87,7 +87,7 @@ export const ListDragOverReactions = {
|
||||
|
||||
export interface IListDragAndDrop<T> {
|
||||
getDragURI(element: T): string | null;
|
||||
getDragLabel?(elements: T[]): string;
|
||||
getDragLabel?(elements: T[]): string | undefined;
|
||||
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void;
|
||||
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
|
||||
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
|
||||
|
||||
@@ -611,11 +611,13 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
event.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([uri]));
|
||||
|
||||
if (event.dataTransfer.setDragImage) {
|
||||
let label: string;
|
||||
let label: string | undefined;
|
||||
|
||||
if (this.dnd.getDragLabel) {
|
||||
label = this.dnd.getDragLabel(elements);
|
||||
} else {
|
||||
}
|
||||
|
||||
if (typeof label === 'undefined') {
|
||||
label = String(elements.length);
|
||||
}
|
||||
|
||||
@@ -721,6 +723,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
private onDragLeave(): void {
|
||||
this.onDragLeaveTimeout.dispose();
|
||||
this.onDragLeaveTimeout = DOM.timeout(() => this.clearDragOverFeedback(), 100);
|
||||
}
|
||||
|
||||
@@ -750,6 +753,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
private clearDragOverFeedback(): void {
|
||||
this.currentDragFeedback = undefined;
|
||||
this.currentDragFeedbackDisposable.dispose();
|
||||
this.currentDragFeedbackDisposable = Disposable.None;
|
||||
}
|
||||
|
||||
// DND scroll top animation
|
||||
|
||||
@@ -16,8 +16,8 @@ import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop } from './list';
|
||||
import { ListView, IListViewOptions } from './listView';
|
||||
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction } from './list';
|
||||
import { ListView, IListViewOptions, IListViewDragAndDrop } from './listView';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
@@ -25,6 +25,7 @@ import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
|
||||
import { clamp } from 'vs/base/common/numbers';
|
||||
import { matchesPrefix } from 'vs/base/common/filters';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
|
||||
interface ITraitChangeEvent {
|
||||
indexes: number[];
|
||||
@@ -940,6 +941,37 @@ class AccessibiltyRenderer<T> implements IListRenderer<T, HTMLElement> {
|
||||
}
|
||||
}
|
||||
|
||||
class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
|
||||
|
||||
constructor(private list: List<T>, private dnd: IListDragAndDrop<T>) { }
|
||||
|
||||
getDragElements(element: T): T[] {
|
||||
const selection = this.list.getSelectedElements();
|
||||
const elements = selection.indexOf(element) > -1 ? selection : [element];
|
||||
return elements;
|
||||
}
|
||||
|
||||
getDragURI(element: T): string | null {
|
||||
return this.dnd.getDragURI(element);
|
||||
}
|
||||
|
||||
getDragLabel?(elements: T[]): string | undefined {
|
||||
return this.dnd.getDragLabel && this.dnd.getDragLabel(elements);
|
||||
}
|
||||
|
||||
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
|
||||
this.dnd.onDragStart(data, originalEvent);
|
||||
}
|
||||
|
||||
onDragOver(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): boolean | IListDragOverReaction {
|
||||
return this.dnd.onDragOver(data, targetElement, targetIndex, originalEvent);
|
||||
}
|
||||
|
||||
drop(data: IDragAndDropData, targetElement: T, targetIndex: number, originalEvent: DragEvent): void {
|
||||
this.dnd.drop(data, targetElement, targetIndex, originalEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
private static InstanceCount = 0;
|
||||
@@ -1051,14 +1083,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
const viewOptions: IListViewOptions<T> = {
|
||||
...options,
|
||||
dnd: options.dnd && {
|
||||
...options.dnd,
|
||||
getDragElements: element => {
|
||||
const selection = this.getSelectedElements();
|
||||
const elements = selection.indexOf(element) > -1 ? selection : [element];
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
dnd: options.dnd && new ListViewDragAndDrop(this, options.dnd)
|
||||
};
|
||||
|
||||
this.view = new ListView(container, virtualDelegate, renderers, viewOptions);
|
||||
|
||||
@@ -6,15 +6,71 @@
|
||||
import 'vs/css!./media/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IListOptions, List, IListStyles } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction } from 'vs/base/browser/ui/list/list';
|
||||
import { append, $, toggleClass } from 'vs/base/browser/dom';
|
||||
import { Event, Relay } from 'vs/base/common/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
|
||||
function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
|
||||
class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<ITreeNode<T, TFilterData>> {
|
||||
|
||||
constructor(private modelProvider: () => ITreeModel<T, TFilterData, TRef>, private dnd: ITreeDragAndDrop<T>) { }
|
||||
|
||||
getDragURI(node: ITreeNode<T, TFilterData>): string | null {
|
||||
return this.dnd.getDragURI(node.element);
|
||||
}
|
||||
|
||||
getDragLabel(nodes: ITreeNode<T, TFilterData>[]): string | undefined {
|
||||
return this.dnd.getDragLabel && this.dnd.getDragLabel(nodes.map(node => node.element));
|
||||
}
|
||||
|
||||
onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
|
||||
this.dnd.onDragStart(data, originalEvent);
|
||||
}
|
||||
|
||||
onDragOver(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent, force = false): boolean | IListDragOverReaction {
|
||||
const result = this.dnd.onDragOver(data, targetNode && targetNode.element, targetIndex, originalEvent);
|
||||
|
||||
if (typeof targetNode === 'undefined') {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined') {
|
||||
if (force) {
|
||||
const accept = typeof result === 'boolean' ? result : result.accept;
|
||||
const effect = typeof result === 'boolean' ? undefined : result.effect;
|
||||
return { accept, effect, feedback: [targetIndex!] };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.bubble === TreeDragOverBubble.Up) {
|
||||
const parentNode = targetNode.parent;
|
||||
const model = this.modelProvider();
|
||||
const parentIndex = parentNode && model.getListIndex(model.getNodeLocation(parentNode));
|
||||
|
||||
return this.onDragOver(data, parentNode, parentIndex, originalEvent, true);
|
||||
}
|
||||
|
||||
const model = this.modelProvider();
|
||||
const ref = model.getNodeLocation(targetNode);
|
||||
const start = model.getListIndex(ref);
|
||||
const length = model.getListRenderCount(ref);
|
||||
|
||||
return { ...result, feedback: range(start, start + length) };
|
||||
}
|
||||
|
||||
drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
|
||||
this.dnd.drop(data, targetNode && targetNode.element, targetIndex, originalEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
|
||||
return options && {
|
||||
...options,
|
||||
identityProvider: options.identityProvider && {
|
||||
@@ -22,23 +78,7 @@ function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilter
|
||||
return options.identityProvider!.getId(el.element);
|
||||
}
|
||||
},
|
||||
dnd: options.dnd && {
|
||||
getDragURI(node) {
|
||||
return options.dnd!.getDragURI(node.element);
|
||||
},
|
||||
getDragLabel: options.dnd!.getDragLabel && ((nodes) => {
|
||||
return options.dnd!.getDragLabel!(nodes.map(node => node.element));
|
||||
}),
|
||||
onDragStart(data, originalEvent) {
|
||||
return options.dnd!.onDragStart(data, originalEvent);
|
||||
},
|
||||
onDragOver(data, targetNode, targetIndex, originalEvent) {
|
||||
return options.dnd!.onDragOver(data, targetNode && targetNode.element, targetIndex, originalEvent);
|
||||
},
|
||||
drop(data, targetNode, targetIndex, originalEvent) {
|
||||
return options.dnd!.drop(data, targetNode && targetNode.element, targetIndex, originalEvent);
|
||||
}
|
||||
},
|
||||
dnd: options.dnd && new TreeNodeListDragAndDrop(modelProvider, options.dnd),
|
||||
multipleSelectionController: options.multipleSelectionController && {
|
||||
isSelectionSingleChangeEvent(e) {
|
||||
return options.multipleSelectionController!.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
|
||||
@@ -239,7 +279,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
const treeRenderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event));
|
||||
this.disposables.push(...treeRenderers);
|
||||
|
||||
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(options));
|
||||
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(() => this.model, options));
|
||||
|
||||
this.model = this.createModel(this.view, options);
|
||||
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
|
||||
|
||||
@@ -127,6 +127,10 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
return this.getTreeNodeWithListIndex(location).listIndex;
|
||||
}
|
||||
|
||||
getListRenderCount(location: number[]): number {
|
||||
return this.getTreeNode(location).renderNodeCount;
|
||||
}
|
||||
|
||||
isCollapsible(location: number[]): boolean {
|
||||
return this.getTreeNode(location).collapsible;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
return this.model.getListIndex(location);
|
||||
}
|
||||
|
||||
getListRenderCount(element: T): number {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.getListRenderCount(location);
|
||||
}
|
||||
|
||||
isCollapsible(element: T): boolean {
|
||||
const location = this.getElementLocation(element);
|
||||
return this.model.isCollapsible(location);
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
|
||||
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
|
||||
|
||||
getListIndex(location: TRef): number;
|
||||
getListRenderCount(location: TRef): number;
|
||||
getNode(location?: TRef): ITreeNode<T, any>;
|
||||
getNodeLocation(node: ITreeNode<T, any>): TRef;
|
||||
getParentNodeLocation(location: TRef): TRef;
|
||||
|
||||
Reference in New Issue
Block a user