* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2025 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
import * as React from 'react';
import { useEffect, useRef } from 'react';
import { computed, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { preOrderPinnedFlatten } from '../../entity/common';
import type { TreeNode } from '../../entity/common';
import type { InsightUnit } from '../../entity/insight';
import styled from '@emotion/styled';
import type { Session } from '../../entity/session';
import { getAutoKey } from '../../utils/dataAutoKey';
import { Unit, Scroller, computeVisibleUnitRange } from './Units';
import { isAncestorPinned, isPinned, isSonPinned } from './unitPin';
import { EventType, useEventBus } from '../../utils/eventBus';
import { PINNED_UNIT_WRAPPER_SCROLLER_ID } from './Units/Units';
const UNIT_MOVE_THRESHOLD = 10;
const PinnedUnitsContainer = styled.div`
z-index: 1;
flex-grow: 1;
`;
const handleWheel = (event: WheelEvent): void => {
if (event.ctrlKey) {
event.preventDefault();
}
};
const INVISIBLE_UNITS_PLACEHOLDER = 'invisible-units-placeholder';
const orderOptions = {
preOrderFlatten: preOrderPinnedFlatten,
options: {
when: (unit: TreeNode<InsightUnit>): boolean => unit.isExpanded,
bypass: (unit: TreeNode<InsightUnit>): boolean => unit.type === 'transparent',
exclude: (unit: TreeNode<InsightUnit>): boolean => !unit.isDisplay || unit.isMultiDeviceHidden,
excludeEx: (unit: TreeNode<InsightUnit>): boolean => isSonPinned(unit),
},
};
const FlattenUnits = observer(({ session, height, laneInfoWidth, eventType }:
{ session: Session; laneInfoWidth: number; height: number; eventType: string }, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element => {
const [scrollTop, setScrollTop] = React.useState(0);
const [nextUnitTop, setNextUnitTop] = React.useState(0);
useEventBus(eventType, (value) => setScrollTop(value as number));
const flattenUnits = computed(() => preOrderPinnedFlatten(session.pinnedUnits, 0, orderOptions.options)).get();
const [first, last] = React.useMemo(
() => computeVisibleUnitRange(flattenUnits, height, scrollTop),
[session.pinnedUnits, flattenUnits, height, scrollTop],
);
const headOffset = React.useMemo(
() => flattenUnits.filter((_, i) => i < first).reduce((prev, cur) => prev + cur.height() + 1, 0),
[flattenUnits, first],
);
const visibleUnitsHeight = React.useMemo(
() => flattenUnits.filter((_, i) => first <= i && i < last).reduce((prev, cur) => prev + cur.height() + 1, 0),
[flattenUnits, first, last],
);
const tailOffset = React.useMemo(
() => flattenUnits.filter((_, i) => i >= last).reduce((prev, cur) => prev + cur.height() + 1, 0),
[flattenUnits, last],
);
const totalHeight = React.useMemo(() => headOffset + visibleUnitsHeight + tailOffset, [headOffset, visibleUnitsHeight, tailOffset]);
const wrapRef = useRef<HTMLDivElement>(null);
const draggingUnitKeyRef = useRef<string>('');
const isUnitBelowTopmostRef = useRef<boolean>(false);
const unitBelowKeyRef = useRef<string>('');
const unitsRefs = useRef(new Map<string, HTMLDivElement | null>());
const [draggingUnitIndex, setDraggingUnitIndex] = React.useState<number>(flattenUnits.length);
const getDraggingUnitIndexByKey = (key: string): void => {
draggingUnitKeyRef.current = key;
const index = flattenUnits.findIndex(item => key === getAutoKey(item));
setDraggingUnitIndex(index !== -1 ? index : flattenUnits.length);
};
const switchUnitInSession = (dragKey: string, nextKey: string): void => {
runInAction(() => {
const pinnedUnits = session.pinnedUnits;
const draggingIndex = pinnedUnits.findIndex(item => dragKey === getAutoKey(item));
const nextDownIndex = pinnedUnits.findIndex(item => nextKey === getAutoKey(item));
if (draggingIndex !== -1 && nextDownIndex !== -1) {
const draggingUnit = pinnedUnits.splice(draggingIndex, 1);
pinnedUnits.splice(nextDownIndex, 0, draggingUnit[0]);
}
});
};
const onMouseMove = (e: MouseEvent, isMoveDown: boolean): void => {
for (const [key, value] of unitsRefs.current) {
const rect = value?.getBoundingClientRect();
if (!rect) {
continue;
}
if (isMoveDown) {
if (!(rect.bottom - e.clientY < UNIT_MOVE_THRESHOLD && rect.bottom > e.clientY)) {
continue;
}
const movingUnitIndex = flattenUnits.findIndex(item => key === getAutoKey(item));
if (!isAncestorPinned(flattenUnits[movingUnitIndex])) {
unitBelowKeyRef.current = key;
isUnitBelowTopmostRef.current = true;
}
if (isUnitBelowTopmostRef.current &&
(movingUnitIndex === flattenUnits.length - 1 || !isAncestorPinned(flattenUnits[movingUnitIndex + 1]))) {
isUnitBelowTopmostRef.current = false;
switchUnitInSession(draggingUnitKeyRef.current, unitBelowKeyRef.current);
setDraggingUnitIndex(movingUnitIndex);
}
break;
} else {
if (!(e.clientY - rect.top < UNIT_MOVE_THRESHOLD && e.clientY > rect.top)) {
continue;
}
const movingUnitIndex = flattenUnits.findIndex(item => key === getAutoKey(item));
if (!isAncestorPinned(flattenUnits[movingUnitIndex])) {
switchUnitInSession(draggingUnitKeyRef.current, key);
setDraggingUnitIndex(movingUnitIndex - 1);
}
break;
}
}
};
const getUnits = (): JSX.Element[] => {
const units = [];
for (let i = 0; i < flattenUnits.length; i++) {
if (getAutoKey(flattenUnits[i]) === draggingUnitKeyRef.current) {
units.push(<Unit
key={getAutoKey(flattenUnits[i])}
laneInfoWidth={laneInfoWidth}
unit={flattenUnits[i]}
session={session}
hasPinButton={true}
hasExpandIcon={true}
isVisible={true}
isPinned={isPinned(flattenUnits[i])}
isSonPinned={isSonPinned(flattenUnits[i])}
enableDrag={!isAncestorPinned(flattenUnits[i])}
getDraggingUnitIndexByKey={getDraggingUnitIndexByKey}
setNextUnitTop={setNextUnitTop}
onMouseMove={onMouseMove}
/>);
continue;
}
if (first <= i && i <= last) {
units.push(<Unit
ref={(el): void => {
unitsRefs.current.set(getAutoKey(flattenUnits[i]), el);
}}
key={getAutoKey(flattenUnits[i])}
laneInfoWidth={laneInfoWidth}
unit={flattenUnits[i]}
session={session}
hasPinButton={true}
hasExpandIcon={true}
isVisible={true}
isPinned={isPinned(flattenUnits[i])}
isSonPinned={isSonPinned(flattenUnits[i])}
// 拖拽泳道下方泳道全部向下偏移拖拽泳道的高度,以保障拖拽泳道脱离文档流的整体布局不变
nextUnitTop={i > draggingUnitIndex ? nextUnitTop : 0}
enableDrag={!isAncestorPinned(flattenUnits[i])}
getDraggingUnitIndexByKey={getDraggingUnitIndexByKey}
setNextUnitTop={setNextUnitTop}
onMouseMove={onMouseMove}
/>);
}
}
return units;
};
useEffect(() => {
const element = wrapRef.current;
if (element) {
element.addEventListener('wheel', handleWheel, { passive: false });
return () => { element.removeEventListener('wheel', handleWheel); };
}
return () => {};
}, []);
return <PinnedUnitsContainer ref={wrapRef} style={{ display: 'flex', flexDirection: 'column', height: totalHeight }}>
<div className={INVISIBLE_UNITS_PLACEHOLDER} style={{ height: headOffset }} />
{getUnits()}
<div className={INVISIBLE_UNITS_PLACEHOLDER} style={{ height: tailOffset }} />
</PinnedUnitsContainer>;
});
const PUnits = ({ session, height, laneInfoWidth }:
{ session: Session; height: number; laneInfoWidth: number }, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element => {
return <Scroller id={PINNED_UNIT_WRAPPER_SCROLLER_ID} session={session} unitsArea={session.pinnedUnits} supportJump={false}
ref={ref} orderOptions={orderOptions} eventType={EventType.PINNEDUNITWRAPPERSCROLL}>
<FlattenUnits
session={session}
height={height}
laneInfoWidth={laneInfoWidth}
eventType={EventType.PINNEDUNITWRAPPERSCROLL} />
</Scroller>;
};
export const PinnedUnits = React.forwardRef(PUnits);