diff --git a/examples/demo/App.tsx b/examples/demo/App.tsx index a2e04a33..f218cdee 100755 --- a/examples/demo/App.tsx +++ b/examples/demo/App.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import * as ReactDOM from "react-dom"; +import { v4 } from "uuid"; import * as FlexLayout from "../../src/index"; import { Action, Actions, BorderNode, DropInfo, IJsonTabNode, Node, TabNode, TabSetNode } from "../../src/index"; import { ILayoutProps, ITabRenderValues, ITabSetRenderValues } from "../../src/view/Layout"; @@ -341,9 +342,9 @@ class App extends React.Component { + onTabDrag={(dragging, over, x, y, location, refresh) => { const tabStorageImpl = over.getExtraData().tabStorage_onTabDrag as ILayoutProps['onTabDrag'] - if (tabStorageImpl) return tabStorageImpl(dragging, over, x, y, location) + if (tabStorageImpl) return tabStorageImpl(dragging, over, x, y, location, refresh) return undefined }} // classNameMapper={ @@ -497,9 +498,42 @@ function TabStorage({ tab, layout }: { tab: TabNode, layout: FlexLayout.Layout } const refs = useRef>(new Map()).current const [emptyElem, setEmptyElem] = useState(null) + // true = down, false = up, null = none + const [scrollDown, setScrollDown] = useState(null) + + const scrollInvalidateRef = useRef<() => void>() + const scroller = useCallback((isDown: boolean) => { + contents?.scrollBy(0, isDown ? 10 : -10) + scrollInvalidateRef.current?.() + }, [contents]) + + const scrollerRef = useRef(scroller) + scrollerRef.current = scroller + + useEffect(() => { + if (scrollDown !== null) { + let scrollInterval: NodeJS.Timeout + let scrollTimeout = setTimeout(() => { + scrollerRef.current(scrollDown) + scrollInterval = setInterval(() => scrollerRef.current(scrollDown), 50) + }, 500) + + return () => { + clearTimeout(scrollTimeout) + clearInterval(scrollInterval) + } + } + + return + }, [scrollDown]) + const kickstartingCallback = useCallback((dragging: TabNode | IJsonTabNode) => { const json = dragging instanceof TabNode ? dragging.toJson() as IJsonTabNode : dragging + if (json.id === undefined) { + json.id = `#${v4()}` + } + setStoredTabs(tabs => [...tabs, json]) if (dragging instanceof TabNode) { @@ -537,6 +571,10 @@ function TabStorage({ tab, layout }: { tab: TabNode, layout: FlexLayout.Layout } const { insertionIndex } = calculateInsertion(absoluteY) const json = dragging instanceof TabNode ? dragging.toJson() as IJsonTabNode : dragging + if (json.id === undefined) { + json.id = `#${v4()}` + } + setStoredTabs(tabs => { const newTabs = [...tabs] const foundAt = newTabs.indexOf(json) @@ -551,12 +589,14 @@ function TabStorage({ tab, layout }: { tab: TabNode, layout: FlexLayout.Layout } return newTabs }) + setScrollDown(null) + if (dragging instanceof TabNode) { tab.getModel().doAction(Actions.deleteTab(dragging.getId())); } }, [calculateInsertion, tab, layout]) - tab.getExtraData().tabStorage_onTabDrag = useCallback(((dragging, over, x, y) => { + tab.getExtraData().tabStorage_onTabDrag = useCallback(((dragging, over, x, y, _, refresh) => { if (contents && list) { const layoutDomRect = layout.getDomRect() const tabRect = tab.getRect() @@ -582,12 +622,23 @@ function TabStorage({ tab, layout }: { tab: TabNode, layout: FlexLayout.Layout } } else { const insertion = calculateInsertion(absY) + scrollInvalidateRef.current = refresh + + if (absY - rootY < tabRect.height / 5) { + setScrollDown(false) + } else if (absY - rootY > tabRect.height * 4/5) { + setScrollDown(true) + } else { + setScrollDown(null) + } + return { x: listBounds.left - rootX, y: insertion.split - rootY - 2, // -2 needed for border thickness, TODO: have flexlayout automatically make this unnecessary for 0-height/width borders width: listBounds.width, height: 0, callback: insertionCallback, + invalidated: () => setScrollDown(null), cursor: 'row-resize' } } diff --git a/src/view/Layout.tsx b/src/view/Layout.tsx index 407cfe4b..be715f46 100755 --- a/src/view/Layout.tsx +++ b/src/view/Layout.tsx @@ -25,6 +25,8 @@ import { FloatingWindowTab } from "./FloatingWindowTab"; import { TabFloating } from "./TabFloating"; import { IJsonTabNode } from "../model/IJsonModel"; +export type CustomDragCallback = (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) => void; + export interface ILayoutProps { model: Model; factory: (node: TabNode) => React.ReactNode; @@ -54,12 +56,14 @@ export interface ILayoutProps { supportsPopout?: boolean | undefined; popoutURL?: string | undefined; realtimeResize?: boolean | undefined; - onTabDrag?: (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) => undefined | { + onTabDrag?: (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation, refresh: () => void) => undefined | { x: number, y: number, width: number, height: number, - callback: (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) => void, + callback: CustomDragCallback, + // Called once when `callback` is not going to be called anymore (user canceled the drag, moved mouse and you returned a different callback, etc) + invalidated?: () => void, cursor?: string | undefined }; } @@ -108,7 +112,8 @@ export interface IIcons { export interface ICustomDropDestination { rect: Rect; - callback: (dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) => void; + callback: CustomDragCallback; + invalidated: (() => void) | undefined; dragging: TabNode | IJsonTabNode; over: TabNode; x: number; @@ -390,8 +395,8 @@ export class Layout extends React.Component { } /** @hidden @internal */ - onTabDrag(dragging: TabNode | IJsonTabNode, over: TabNode, x: number, y: number, location: DockLocation) { - return this.props.onTabDrag?.(dragging, over, x, y, location); + onTabDrag(...args: Parameters['onTabDrag']>) { + return this.props.onTabDrag?.(...args); } /** @hidden @internal */ @@ -691,8 +696,16 @@ export class Layout extends React.Component { this.fnNewNodeDropped(); this.fnNewNodeDropped = undefined; } + + try { + this.customDrop?.invalidated?.() + } catch (e) { + console.error(e) + } + DragDrop.instance.hideGlass(); this.newTabJson = undefined; + this.customDrop = undefined; }; /** @hidden @internal */ @@ -714,8 +727,16 @@ export class Layout extends React.Component { this.fnNewNodeDropped(); this.fnNewNodeDropped = undefined; } + + try { + this.customDrop?.invalidated?.() + } catch (e) { + console.error(e) + } + DragDrop.instance.hideGlass(); this.newTabJson = undefined; + this.customDrop = undefined; } }; @@ -788,6 +809,8 @@ export class Layout extends React.Component { let dropInfo = this.props.model._findDropTargetNode(this.dragNode!, pos.x, pos.y); if (dropInfo) { + let invalidated = this.customDrop?.invalidated; + const currentCallback = this.customDrop?.callback; this.customDrop = undefined; const dragging = this.newTabJson || (this.dragNode instanceof TabNode ? this.dragNode : undefined); @@ -799,12 +822,13 @@ export class Layout extends React.Component { let customDrop: ICustomDropDestination | undefined = undefined; try { - const dest = this.onTabDrag(dragging, selected, pos.x - tabRect.x, pos.y - tabRect.y, dropInfo.location); + const dest = this.onTabDrag(dragging, selected, pos.x - tabRect.x, pos.y - tabRect.y, dropInfo.location, () => this.onDragMove(event)); if (dest) { customDrop = { rect: new Rect(dest.x + tabRect.x, dest.y + tabRect.y, dest.width, dest.height), callback: dest.callback, + invalidated: dest.invalidated, dragging: dragging, over: selected, x: pos.x - tabRect.x, @@ -817,6 +841,10 @@ export class Layout extends React.Component { console.error(e) } + if (customDrop?.callback == currentCallback) { + invalidated = undefined; + } + this.customDrop = customDrop; } } @@ -832,6 +860,12 @@ export class Layout extends React.Component { DragDrop.instance.setGlassCursorOverride(this.customDrop?.cursor); this.outlineDiv!.style.visibility = "visible"; + + try { + invalidated?.(); + } catch (e) { + console.error(e); + } } };