Drag and Drop players in the editor #11
Merged
maxime.batista
merged 12 commits from editor/place-players
into master
1 year ago
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 427 B |
@ -0,0 +1,58 @@
|
|||||||
|
import {ReactElement, useRef} from "react";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
|
||||||
|
export interface RackProps<E extends {key: string | number}> {
|
||||||
|
id: string,
|
||||||
|
objects: E[],
|
||||||
|
onChange: (objects: E[]) => void,
|
||||||
|
canDetach: (ref: HTMLDivElement) => boolean,
|
||||||
|
onElementDetached: (ref: HTMLDivElement, el: E) => void,
|
||||||
|
render: (e: E) => ReactElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RackItemProps<E extends {key: string | number}> {
|
||||||
|
item: E,
|
||||||
|
onTryDetach: (ref: HTMLDivElement, el: E) => void,
|
||||||
|
render: (e: E) => ReactElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container of draggable objects
|
||||||
|
* */
|
||||||
|
export function Rack<E extends {key: string | number}>({id, objects, onChange, canDetach, onElementDetached, render}: RackProps<E>) {
|
||||||
|
return (
|
||||||
|
<div id={id} style={{
|
||||||
|
display: "flex"
|
||||||
|
}}>
|
||||||
|
{objects.map(element => (
|
||||||
|
<RackItem key={element.key}
|
||||||
|
item={element}
|
||||||
|
render={render}
|
||||||
|
onTryDetach={(ref, element) => {
|
||||||
|
if (!canDetach(ref))
|
||||||
|
return
|
||||||
|
|
||||||
|
const index = objects.findIndex(o => o.key === element.key)
|
||||||
|
onChange(objects.toSpliced(index, 1))
|
||||||
|
|
||||||
|
onElementDetached(ref, element)
|
||||||
|
}}/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RackItem<E extends {key: string | number}>({item, onTryDetach, render}: RackItemProps<E>) {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
position={{x: 0, y: 0}}
|
||||||
|
nodeRef={divRef}
|
||||||
|
onStop={() => onTryDetach(divRef.current!, item)}>
|
||||||
|
<div ref={divRef}>
|
||||||
|
{render(item)}
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import CourtSvg from '../../assets/basketball_court.svg?react';
|
||||||
|
import '../../style/basket_court.css';
|
||||||
|
import {useRef} from "react";
|
||||||
|
import CourtPlayer from "./CourtPlayer";
|
||||||
|
import {Player} from "../../data/Player";
|
||||||
|
|
||||||
|
export interface BasketCourtProps {
|
||||||
|
players: Player[],
|
||||||
|
onPlayerRemove: (p: Player) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) {
|
||||||
|
return (
|
||||||
|
<div id="court-container" style={{position: "relative"}}>
|
||||||
|
<CourtSvg id="court-svg"/>
|
||||||
|
{players.map(player => {
|
||||||
|
return <CourtPlayer key={player.id}
|
||||||
|
player={player}
|
||||||
|
onRemove={() => onPlayerRemove(player)}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
|||||||
|
import {useRef} from "react";
|
||||||
|
import "../../style/player.css";
|
||||||
|
import RemoveIcon from "../../assets/icon/remove.svg?react";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
import {PlayerPiece} from "./PlayerPiece";
|
||||||
|
import {Player} from "../../data/Player";
|
||||||
|
|
||||||
|
export interface PlayerProps {
|
||||||
|
player: Player,
|
||||||
|
onRemove: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A player that is placed on the court, which can be selected, and moved in the associated bounds
|
||||||
|
* */
|
||||||
|
export default function CourtPlayer({player, onRemove}: PlayerProps) {
|
||||||
|
|
||||||
|
const x = player.rightRatio;
|
||||||
|
const y = player.bottomRatio;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable
|
||||||
|
handle={".player-piece"}
|
||||||
|
bounds="parent"
|
||||||
|
>
|
||||||
|
<div className={"player"}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: `${x * 100}%`,
|
||||||
|
top: `${y * 100}%`,
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<div tabIndex={0}
|
||||||
|
className="player-content"
|
||||||
|
onKeyUp={e => {
|
||||||
|
if (e.key == "Delete")
|
||||||
|
onRemove()
|
||||||
|
}}>
|
||||||
|
<div className="player-selection-tab">
|
||||||
|
<RemoveIcon
|
||||||
|
className="player-selection-tab-remove"
|
||||||
|
onClick={onRemove}/>
|
||||||
|
</div>
|
||||||
|
<PlayerPiece team={player.team} text={player.role}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import '../../style/player.css'
|
||||||
|
import {Team} from "../../data/Team";
|
||||||
|
|
||||||
|
|
||||||
|
export function PlayerPiece({team, text}: { team: Team, text: string }) {
|
||||||
|
return (
|
||||||
|
<div className={`player-piece ${team}`}>
|
||||||
|
<p>{text}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import {Team} from "./Team";
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
/**
|
||||||
|
* unique identifier of the player.
|
||||||
|
* This identifier must be unique to the associated court.
|
||||||
|
*/
|
||||||
|
id: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the player's team
|
||||||
|
* */
|
||||||
|
team: Team,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* player's position
|
||||||
|
* */
|
||||||
|
role: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
|
||||||
|
*/
|
||||||
|
bottomRatio: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
|
||||||
|
*/
|
||||||
|
rightRatio: number,
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum Team {
|
||||||
|
Allies = "allies",
|
||||||
|
Opponents = "opponents"
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#court-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
|
||||||
|
background-color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#court-svg {
|
||||||
|
margin: 5%;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#court-svg * {
|
||||||
|
stroke: var(--selected-team-secondarycolor);
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
as the .player div content is translated,
|
||||||
|
the real .player div position is not were the user can expect.
|
||||||
|
Disable pointer events to this div as it may overlap on other components
|
||||||
|
on the court.
|
||||||
|
*/
|
||||||
|
.player {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-piece {
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
background-color: var(--selected-team-primarycolor);
|
||||||
|
color: var(--selected-team-secondarycolor);
|
||||||
|
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: 100px;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
margin-bottom: 10%;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove {
|
||||||
|
pointer-events: all;
|
||||||
|
height: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove * {
|
||||||
|
stroke: red;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-selection-tab-remove:hover * {
|
||||||
|
fill: #f1dbdb;
|
||||||
|
stroke: #ff331a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within .player-selection-tab {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within .player-piece {
|
||||||
|
color: var(--selection-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player:focus-within {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
../front
|
Loading…
Reference in new issue