Merge pull request 'Drag and Drop players in the editor' (#11) from editor/place-players into master
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #11pull/13/head
commit
582a623576
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,55 @@
|
||||
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 ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const x = player.rightRatio;
|
||||
const y = player.bottomRatio;
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
handle={".player-piece"}
|
||||
nodeRef={ref}
|
||||
bounds="parent"
|
||||
>
|
||||
<div ref={ref}
|
||||
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