This commit is contained in:
WolverinDEV 2020-09-25 20:55:26 +02:00 committed by WolverinDEV
parent ca875258b4
commit 2b2681b2e7
4 changed files with 431 additions and 133 deletions

View file

@ -57,6 +57,19 @@ function initializeController(events: Registry<ConnectionListUIEvents>) {
events.fire_async("query_handler_list"); events.fire_async("query_handler_list");
})); }));
events.on("notify_destroy", server_connections.events().on("notify_handler_order_changed", () => events.fire_async("query_handler_list")));
events.on("action_swap_handler", event => {
const handlerA = server_connections.findConnection(event.handlerIdOne);
const handlerB = server_connections.findConnection(event.handlerIdTwo);
if(!handlerA || !handlerB) {
logWarn(LogCategory.CLIENT, tr("Tried to switch handler %s with %s, but one of them does not exists"), event.handlerIdOne, event.handlerIdTwo);
return;
}
server_connections.swapHandlerOrder(handlerA, handlerB);
});
events.on("action_set_active_handler", event => { events.on("action_set_active_handler", event => {
const handler = server_connections.findConnection(event.handlerId); const handler = server_connections.findConnection(event.handlerId);

View file

@ -1,5 +1,6 @@
import {LocalIcon} from "tc-shared/file/Icons"; import {LocalIcon} from "tc-shared/file/Icons";
export type MouseMoveCoordinates = { x: number, y: number, xOffset: number };
export type HandlerConnectionState = "disconnected" | "connecting" | "connected"; export type HandlerConnectionState = "disconnected" | "connecting" | "connected";
export type HandlerStatus = { export type HandlerStatus = {
@ -12,7 +13,19 @@ export type HandlerStatus = {
export interface ConnectionListUIEvents { export interface ConnectionListUIEvents {
action_set_active_handler: { handlerId: string }, action_set_active_handler: { handlerId: string },
action_destroy_handler: { handlerId: string }, action_destroy_handler: { handlerId: string },
action_scroll: { direction: "left" | "right" } action_scroll: { direction: "left" | "right" },
action_move_handler: {
handlerId: string | undefined,
mouse?: MouseMoveCoordinates
},
action_set_moving_position: {
offsetX: number,
width: number
},
action_swap_handler: {
handlerIdOne: string,
handlerIdTwo: string
}
query_handler_status: { handlerId: string }, query_handler_status: { handlerId: string },
query_handler_list: {}, query_handler_list: {},

View file

@ -23,9 +23,66 @@
position: relative; position: relative;
.handlerList { .containerScroll {
margin-top: 5px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: none;
flex-direction: row;
&.shown {
display: flex;
}
.buttonScroll {
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
border: 1px solid;
@include hex-rgba(border-color, #2222223b);
border-radius: 2px;
background: #e7e7e7;
padding-left: 2px;
padding-right: 2px;
&:hover {
background: #eeeeee;
}
&.disabled {
background: #9e9e9e;
&:hover {
background: #9e9e9e;
}
}
}
}
&.scrollShown {
/*
.connection-handlers {
width: calc(100% - 45px);
}
*/
.handlerList .scrollSpacer {
display: block;
}
}
}
.handlerList {
height: 100%; height: 100%;
width: fit-content; width: 100%;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -37,6 +94,8 @@
max-width: 100%; max-width: 100%;
scroll-behavior: smooth; scroll-behavior: smooth;
position: relative;
.handler { .handler {
padding-top: 4px; padding-top: 4px;
position: relative; position: relative;
@ -54,7 +113,7 @@
overflow: hidden; overflow: hidden;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
@include transition(all ease-in-out $button_hover_animation_time); @include transition(all ease-in-out $button_hover_animation_time, opacity ease-in-out 0);
.icon { .icon {
width: 16px; width: 16px;
@ -116,7 +175,7 @@
} }
} }
&.active { &.mode-active {
border-bottom-color: #0d9cfd; border-bottom-color: #0d9cfd;
background-color: #2d2f32; background-color: #2d2f32;
@ -125,6 +184,12 @@
} }
} }
&.mode-normal { /* nothing */ }
&.mode-spacer {
opacity: 0;
}
&.audioPlayback { &.audioPlayback {
border-bottom-color: #68c1fd; border-bottom-color: #68c1fd;
} }
@ -140,64 +205,26 @@
flex-grow: 0; flex-grow: 0;
} }
&.hardScroll {
scroll-behavior: unset;
}
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
} }
.containerScroll { .moveContainer {
margin-top: 5px;
position: absolute; position: absolute;
left: 4em;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0;
display: none; display: none;
flex-direction: row; flex-direction: row;
&.shown { width: min-content;
display: flex;
}
.buttonScroll { background: #1e1e1e;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
border: 1px solid;
@include hex-rgba(border-color, #2222223b);
border-radius: 2px;
background: #e7e7e7;
padding-left: 2px;
padding-right: 2px;
&:hover {
background: #eeeeee;
}
&.disabled {
background: #9e9e9e;
&:hover {
background: #9e9e9e;
}
}
}
}
&.scrollShown {
/*
.connection-handlers {
width: calc(100% - 45px);
}
*/
.handlerList .scrollSpacer {
display: block;
}
}
} }

View file

@ -1,5 +1,9 @@
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {ConnectionListUIEvents, HandlerStatus} from "tc-shared/ui/frames/connection-handler-list/Definitions"; import {
ConnectionListUIEvents,
HandlerStatus,
MouseMoveCoordinates
} from "tc-shared/ui/frames/connection-handler-list/Definitions";
import * as React from "react"; import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
@ -13,8 +17,8 @@ import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
const cssStyle = require("./Renderer.scss"); const cssStyle = require("./Renderer.scss");
const Events = React.createContext<Registry<ConnectionListUIEvents>>(undefined); const Events = React.createContext<Registry<ConnectionListUIEvents>>(undefined);
type ConnectionHandlerMode = "normal" | "active" | "spacer";
const ConnectionHandler = (props: { handlerId: string, active: boolean }) => { const ConnectionHandler = React.memo((props: { handlerId: string, mode: ConnectionHandlerMode, refContainer?: React.Ref<HTMLDivElement> }) => {
const events = useContext(Events); const events = useContext(Events);
const [ status, setStatus ] = useState<HandlerStatus | "loading">(() => { const [ status, setStatus ] = useState<HandlerStatus | "loading">(() => {
@ -53,19 +57,22 @@ const ConnectionHandler = (props: { handlerId: string, active: boolean }) => {
case "disconnected": case "disconnected":
displayedName = <Translatable key={"not connected"}>Not connected</Translatable>; displayedName = <Translatable key={"not connected"}>Not connected</Translatable>;
displayedName = props.handlerId;
break; break;
} }
} }
return ( return (
<div className={cssStyle.handler + " " + (props.active ? cssStyle.active : "") + " " + (cutoffName ? cssStyle.cutoffName : "") + " " + (voiceReplaying ? cssStyle.audioPlayback : "")} <div className={cssStyle.handler + " " + cssStyle["mode-" + props.mode] + " " + (cutoffName ? cssStyle.cutoffName : "") + " " + (voiceReplaying ? cssStyle.audioPlayback : "")}
onClick={() => { onClick={() => {
if(props.active) { if(props.mode !== "normal") {
return; return;
} }
events.fire("action_set_active_handler", { handlerId: props.handlerId }); events.fire("action_set_active_handler", { handlerId: props.handlerId });
}} }}
ref={props.refContainer}
x-handler-id={props.handlerId}
> >
<div className={cssStyle.icon}> <div className={cssStyle.icon}>
{icon} {icon}
@ -78,9 +85,190 @@ const ConnectionHandler = (props: { handlerId: string, active: boolean }) => {
</div> </div>
</div> </div>
); );
} })
const HandlerList = (props: { refContainer: React.Ref<HTMLDivElement>, refSpacer: React.Ref<HTMLDivElement> }) => { const MoveableConnectionHandler = (props: { handlerId: string, mode: ConnectionHandlerMode }) => {
const events = useContext(Events);
const refContainer = useRef<HTMLDivElement>();
useEffect(() => {
if(!refContainer.current) {
return;
}
const attached = { status: false };
const basePoint = { x: 0, y: 0 };
const mouseDownListener = (event: MouseEvent) => {
basePoint.x = event.pageX;
basePoint.y = event.pageY;
attachListener();
};
const mouseMoveListener = (event: MouseEvent) => {
const diffXSqrt = Math.abs(basePoint.x - event.pageX);
if(diffXSqrt > 10) {
events.fire("action_move_handler", {
handlerId: props.handlerId,
mouse: { x: event.pageX, y: event.pageY, xOffset: basePoint.x + refContainer.current.parentElement.scrollLeft - refContainer.current.offsetLeft }
});
detachListener();
}
};
const mouseUpListener = () => detachListener;
const attachListener = () => {
if(attached.status) { return; }
attached.status = true;
document.addEventListener("mousemove", mouseMoveListener);
document.addEventListener("mouseup", mouseUpListener);
document.addEventListener("mouseleave", mouseUpListener);
};
const detachListener = () => {
if(!attached.status) { return; }
attached.status = false;
document.removeEventListener("mousemove", mouseMoveListener);
document.removeEventListener("mouseup", mouseUpListener);
document.removeEventListener("mouseleave", mouseUpListener);
}
refContainer.current.addEventListener("mousedown", mouseDownListener);
return () => {
refContainer.current.removeEventListener("mousedown", mouseDownListener);
detachListener();
}
});
return <ConnectionHandler handlerId={props.handlerId} mode={props.mode} refContainer={refContainer} />
};
const MovingConnectionHandler = React.memo((props: { handlerId: string, handlerActive: boolean, initialMouse: MouseMoveCoordinates }) => {
const events = useContext(Events);
const refContainer = useRef<HTMLDivElement>();
const offsetX = useRef<number>(0);
const mouseXRef = useRef<number>(props.initialMouse.x);
const scrollValue = useRef<number>(0);
const scrollIntervalReference = useRef<number>(0);
useEffect(() => {
if(!refContainer.current ||!refContainer.current.parentElement) { return; }
refContainer.current.style.display = "flex";
let lastMovingEventPositionX = 0;
const executeUpdate = () => {
if(!refContainer.current?.parentElement) { return; }
const parentElement = refContainer.current.parentElement;
updateContainerPosition(mouseXRef.current, parentElement, false);
updateScroll(mouseXRef.current, parentElement);
}
const updateScroll = (mouseX: number, parentElement: HTMLElement) => {
if(mouseX >= parentElement.offsetLeft + parentElement.clientWidth - 100) {
scrollValue.current = +2;
} else if(mouseX <= refContainer.current.parentElement.offsetLeft + 100) {
scrollValue.current = -2;
} else {
scrollValue.current = 0;
}
if(scrollValue.current) {
if(!scrollIntervalReference.current) {
scrollIntervalReference.current = setInterval(() => {
if(!scrollValue.current) {
logWarn(LogCategory.CLIENT, tr("Scroll interval called, but no scroll direction set"));
return;
}
const targetScrollLeft = parentElement.scrollLeft + scrollValue.current;
const cancelScroll = targetScrollLeft < 0 || Math.ceil(targetScrollLeft + parentElement.clientWidth) + 1 >= parentElement.scrollWidth;
if(cancelScroll) {
clearInterval(scrollIntervalReference.current);
scrollIntervalReference.current = -1; /* set, but inactive, needs to be re triggered */
} else {
parentElement.scrollLeft = targetScrollLeft;
}
updateContainerPosition(mouseXRef.current, parentElement, cancelScroll);
}, 5);
parentElement.classList.add(cssStyle.hardScroll);
}
} else {
if(scrollIntervalReference.current) {
clearInterval(scrollIntervalReference.current);
scrollIntervalReference.current = 0;
/* stop the scroll ( + 1 - 1 has been appended for the ide...) */
parentElement.scrollLeft = parentElement.scrollLeft + 1 - 1;
parentElement.classList.remove(cssStyle.hardScroll);
}
}
}
const updateContainerPosition = (mouseX: number, parentElement: HTMLElement, forceFirePositionUpdate: boolean) => {
offsetX.current = (mouseX - parentElement.offsetLeft) - props.initialMouse.xOffset + parentElement.scrollLeft;
if(offsetX.current + props.initialMouse.xOffset < 0) { return; }
if(Math.ceil(offsetX.current + refContainer.current.clientWidth) + 1 >= parentElement.scrollWidth) { return; }
refContainer.current.style.left = offsetX.current + "px";
if(Math.abs(lastMovingEventPositionX - offsetX.current) > 30 || forceFirePositionUpdate) {
lastMovingEventPositionX = offsetX.current;
fireMovingPositionEvent();
}
};
const fireMovingPositionEvent = () => {
events.fire("action_set_moving_position", { offsetX: offsetX.current , width: refContainer.current.clientWidth });
};
const listenerMove = (event: MouseEvent) => {
mouseXRef.current = event.pageX;
executeUpdate();
}
const listenerUp = () => {
events.fire("action_move_handler", { handlerId: undefined });
}
executeUpdate();
document.addEventListener("mousemove", listenerMove);
document.addEventListener("mouseup", listenerUp);
document.addEventListener("mouseleave", listenerUp);
return () => {
clearInterval(scrollIntervalReference.current);
if(refContainer.current?.parentElement) {
const parentElement = refContainer.current?.parentElement;
parentElement.classList.remove(cssStyle.hardScroll);
/* stop the scroll ( + 1 - 1 has been appended for the ide...) */
parentElement.scrollLeft = parentElement.scrollLeft + 1 - 1;
}
document.removeEventListener("mouseup", listenerUp);
document.removeEventListener("mouseleave", listenerUp);
document.removeEventListener("mousemove", listenerMove);
};
});
return (
<div className={cssStyle.moveContainer} ref={refContainer}>
<ConnectionHandler handlerId={props.handlerId} mode={props.handlerActive ? "active" : "normal"} />
</div>
);
});
const HandlerList = (props: { refContainer: React.RefObject<HTMLDivElement>, refSpacer: React.Ref<HTMLDivElement> }) => {
const events = useContext(Events); const events = useContext(Events);
const [ handlers, setHandlers ] = useState<string[] | "loading">(() => { const [ handlers, setHandlers ] = useState<string[] | "loading">(() => {
@ -89,18 +277,73 @@ const HandlerList = (props: { refContainer: React.Ref<HTMLDivElement>, refSpacer
}); });
const [ activeHandler, setActiveHandler ] = useState<string>(); const [ activeHandler, setActiveHandler ] = useState<string>();
const [ movingHandler, setMovingHandler ] = useState<{ handlerId: string, mouse: MouseMoveCoordinates} | undefined>();
const switchRequestTimestamp = useRef(0);
events.reactUse("notify_handler_list", event => { events.reactUse("notify_handler_list", event => {
setHandlers(event.handlerIds.slice()); setHandlers(event.handlerIds.slice());
setActiveHandler(event.activeHandlerId); setActiveHandler(event.activeHandlerId);
}); });
events.reactUse("notify_active_handler", event => setActiveHandler(event.handlerId)); events.reactUse("notify_active_handler", event => setActiveHandler(event.handlerId));
events.reactUse("action_move_handler", event => {
if(typeof event.handlerId === "undefined") {
setMovingHandler(undefined);
} else {
setMovingHandler({ handlerId: event.handlerId, mouse: event.mouse });
}
});
events.reactUse("action_set_moving_position", event => {
if(!movingHandler || handlers === "loading" || (Date.now() - switchRequestTimestamp.current) < 1000) { return; }
const centerCurrent = event.offsetX + event.width / 2;
/* get the target element */
const children = [...props.refContainer.current.childNodes.values()] as HTMLElement[];
const element = children.find(element => element.offsetLeft <= centerCurrent && centerCurrent <= element.offsetLeft + element.clientWidth);
const handlerId = element?.getAttribute("x-handler-id");
if(handlerId === movingHandler.handlerId || !handlerId) { return; }
const oldIndex = handlers.findIndex(handler => handler === movingHandler.handlerId);
const newIndex = handlers.findIndex(handler => handler === handlerId);
const centerTarget = element.offsetLeft + element.clientWidth / 2;
if(oldIndex < newIndex) {
if(event.offsetX + event.width < centerTarget) {
return;
}
} else {
if(event.offsetX > centerTarget) {
return;
}
}
events.fire("action_swap_handler", { handlerIdOne: handlers[oldIndex], handlerIdTwo: handlers[newIndex] });
switchRequestTimestamp.current = Date.now();
});
/* no switch pending, everything has been rendered */
useEffect(() => { switchRequestTimestamp.current = 0; });
return ( return (
<div className={cssStyle.handlerList} ref={props.refContainer}> <div className={cssStyle.handlerList} ref={props.refContainer}>
{handlers === "loading" ? undefined : {handlers === "loading" ? undefined :
handlers.map(handlerId => <ConnectionHandler handlerId={handlerId} key={handlerId} active={handlerId === activeHandler} />) handlers.map(handlerId => (
<MoveableConnectionHandler
handlerId={handlerId}
key={handlerId}
mode={handlerId === movingHandler?.handlerId ? "spacer" : handlerId === activeHandler ? "active" : "normal"}
/>
))
} }
{ movingHandler ? (
<MovingConnectionHandler
handlerId={movingHandler.handlerId}
key={"moving-" + movingHandler.handlerId}
handlerActive={movingHandler.handlerId === activeHandler}
initialMouse={movingHandler.mouse}
/>
) : undefined}
<div className={cssStyle.scrollSpacer} ref={props.refSpacer} /> <div className={cssStyle.scrollSpacer} ref={props.refSpacer} />
</div> </div>
) )
@ -143,6 +386,7 @@ export const ConnectionHandlerList = (props: { events: Registry<ConnectionListUI
const updateScrollButtons = (scrollLeft: number) => { const updateScrollButtons = (scrollLeft: number) => {
const container = refHandlerContainer.current; const container = refHandlerContainer.current;
if(!container) { return; } if(!container) { return; }
props.events.fire_async("notify_scroll_status", { right: Math.ceil(scrollLeft + container.clientWidth + 2) < container.scrollWidth, left: scrollLeft !== 0 }); props.events.fire_async("notify_scroll_status", { right: Math.ceil(scrollLeft + container.clientWidth + 2) < container.scrollWidth, left: scrollLeft !== 0 });
} }
@ -193,6 +437,8 @@ export const ConnectionHandlerList = (props: { events: Registry<ConnectionListUI
} }
const container = refHandlerContainer.current; const container = refHandlerContainer.current;
let scrollLeft;
const scrollContainer = refScrollSpacer.current; const scrollContainer = refScrollSpacer.current;
const getChildAt = (width: number) => { const getChildAt = (width: number) => {
let currentChild: HTMLDivElement; let currentChild: HTMLDivElement;
@ -206,7 +452,6 @@ export const ConnectionHandlerList = (props: { events: Registry<ConnectionListUI
return currentChild; return currentChild;
} }
let scrollLeft;
if(event.direction === "right") { if(event.direction === "right") {
const currentLeft = container.scrollLeft; const currentLeft = container.scrollLeft;
const target = getChildAt(currentLeft + container.clientWidth + 5 - scrollContainer.clientWidth); const target = getChildAt(currentLeft + container.clientWidth + 5 - scrollContainer.clientWidth);