@fuzo/soccer-board 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +335 -0
- package/fesm2022/fuzo-soccer-board.mjs +1021 -0
- package/fesm2022/fuzo-soccer-board.mjs.map +1 -0
- package/package.json +51 -0
- package/types/fuzo-soccer-board.d.ts +347 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, input, output, signal, computed, effect, HostListener, Component, PLATFORM_ID, viewChild, HostBinding } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
+
import html2canvas from 'html2canvas-pro';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Soccer Board Field dimension constants
|
|
8
|
+
* Based on FIFA regulations: 105m x 68m
|
|
9
|
+
*/
|
|
10
|
+
const SoccerBoardFieldConstants = {
|
|
11
|
+
/**
|
|
12
|
+
* Field aspect ratio (width / height)
|
|
13
|
+
* FIFA standard: 105m / 68m ≈ 1.544
|
|
14
|
+
*/
|
|
15
|
+
RATIO: 105 / 68,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Soccer Board Fit Mode types for responsive field sizing
|
|
20
|
+
*/
|
|
21
|
+
var SoccerBoardFitMode;
|
|
22
|
+
(function (SoccerBoardFitMode) {
|
|
23
|
+
SoccerBoardFitMode["Contain"] = "contain";
|
|
24
|
+
})(SoccerBoardFitMode || (SoccerBoardFitMode = {}));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Soccer Board Field Orientation types
|
|
28
|
+
*/
|
|
29
|
+
var SoccerBoardOrientation;
|
|
30
|
+
(function (SoccerBoardOrientation) {
|
|
31
|
+
SoccerBoardOrientation["Portrait"] = "portrait";
|
|
32
|
+
SoccerBoardOrientation["Landscape"] = "landscape";
|
|
33
|
+
})(SoccerBoardOrientation || (SoccerBoardOrientation = {}));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Soccer Board Player Card Size types
|
|
37
|
+
*/
|
|
38
|
+
var SoccerBoardPlayerSize;
|
|
39
|
+
(function (SoccerBoardPlayerSize) {
|
|
40
|
+
SoccerBoardPlayerSize["Small"] = "small";
|
|
41
|
+
SoccerBoardPlayerSize["Medium"] = "medium";
|
|
42
|
+
SoccerBoardPlayerSize["Large"] = "large";
|
|
43
|
+
})(SoccerBoardPlayerSize || (SoccerBoardPlayerSize = {}));
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Soccer Board Team Side types
|
|
47
|
+
*/
|
|
48
|
+
var SoccerBoardTeamSide;
|
|
49
|
+
(function (SoccerBoardTeamSide) {
|
|
50
|
+
SoccerBoardTeamSide["Home"] = "home";
|
|
51
|
+
SoccerBoardTeamSide["Away"] = "away";
|
|
52
|
+
})(SoccerBoardTeamSide || (SoccerBoardTeamSide = {}));
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get tactical position zones data
|
|
56
|
+
* All tactical positions with coordinates for each orientation
|
|
57
|
+
* Portrait: x=left-right, y=center(0)-goal(100)
|
|
58
|
+
* Landscape: x=center(0)-goal(100), y=top(0)-bottom(100)
|
|
59
|
+
*/
|
|
60
|
+
function getPositionsData() {
|
|
61
|
+
// Colors by position type
|
|
62
|
+
const colors = {
|
|
63
|
+
goalkeeper: 'rgba(255, 200, 0, 0.5)', // Yellow
|
|
64
|
+
defense: 'rgba(70, 130, 255, 0.5)', // Blue
|
|
65
|
+
defensiveMid: 'rgba(100, 180, 255, 0.5)', // Light blue
|
|
66
|
+
midfield: 'rgba(50, 200, 100, 0.5)', // Green
|
|
67
|
+
attackingMid: 'rgba(150, 220, 100, 0.5)', // Light green
|
|
68
|
+
forward: 'rgba(255, 100, 100, 0.5)', // Red
|
|
69
|
+
striker: 'rgba(255, 50, 50, 0.5)', // Dark red
|
|
70
|
+
};
|
|
71
|
+
return [
|
|
72
|
+
// ============ DEFENSE ZONE ============
|
|
73
|
+
{
|
|
74
|
+
id: 'LB',
|
|
75
|
+
label: 'LB',
|
|
76
|
+
color: colors.defense,
|
|
77
|
+
portrait: { x: 0, y: 70, width: 20, height: 30 },
|
|
78
|
+
landscape: { x: 70, y: 0, width: 30, height: 20 },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'CB',
|
|
82
|
+
label: 'CB',
|
|
83
|
+
color: colors.defense,
|
|
84
|
+
portrait: { x: 20, y: 70, width: 60, height: 19.5 },
|
|
85
|
+
landscape: { x: 70, y: 20, width: 19.5, height: 60 },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'CB',
|
|
89
|
+
label: '',
|
|
90
|
+
color: colors.defense,
|
|
91
|
+
portrait: { x: 20, y: 89.5, width: 16.55, height: 10.5 },
|
|
92
|
+
landscape: { x: 89.5, y: 20, width: 10.5, height: 16.55 },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'GK',
|
|
96
|
+
label: 'GK',
|
|
97
|
+
color: colors.goalkeeper,
|
|
98
|
+
portrait: { x: 36.55, y: 89.5, width: 26.9, height: 10.5 },
|
|
99
|
+
landscape: { x: 89.5, y: 36.55, width: 10.5, height: 26.9 },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'CB',
|
|
103
|
+
label: '',
|
|
104
|
+
color: colors.defense,
|
|
105
|
+
portrait: { x: 63.45, y: 89.5, width: 16.55, height: 10.5 },
|
|
106
|
+
landscape: { x: 89.5, y: 63.45, width: 10.5, height: 16.55 },
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'RB',
|
|
110
|
+
label: 'RB',
|
|
111
|
+
color: colors.defense,
|
|
112
|
+
portrait: { x: 80, y: 70, width: 20, height: 30 },
|
|
113
|
+
landscape: { x: 70, y: 80, width: 30, height: 20 },
|
|
114
|
+
},
|
|
115
|
+
// ============ DEFENSIVE MIDFIELD ZONE ============
|
|
116
|
+
{
|
|
117
|
+
id: 'LWB',
|
|
118
|
+
label: 'LWB',
|
|
119
|
+
color: colors.defensiveMid,
|
|
120
|
+
portrait: { x: 0, y: 55, width: 25, height: 15 },
|
|
121
|
+
landscape: { x: 55, y: 0, width: 15, height: 25 },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'CDM',
|
|
125
|
+
label: 'CDM',
|
|
126
|
+
color: colors.defensiveMid,
|
|
127
|
+
portrait: { x: 25, y: 55, width: 50, height: 15 },
|
|
128
|
+
landscape: { x: 55, y: 25, width: 15, height: 50 },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'RWB',
|
|
132
|
+
label: 'RWB',
|
|
133
|
+
color: colors.defensiveMid,
|
|
134
|
+
portrait: { x: 75, y: 55, width: 25, height: 15 },
|
|
135
|
+
landscape: { x: 55, y: 75, width: 15, height: 25 },
|
|
136
|
+
},
|
|
137
|
+
// ============ CENTRAL MIDFIELD ZONE ============
|
|
138
|
+
{
|
|
139
|
+
id: 'LM',
|
|
140
|
+
label: 'LM',
|
|
141
|
+
color: colors.midfield,
|
|
142
|
+
portrait: { x: 0, y: 40, width: 25, height: 15 },
|
|
143
|
+
landscape: { x: 40, y: 0, width: 15, height: 25 },
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'CM',
|
|
147
|
+
label: 'CM',
|
|
148
|
+
color: colors.midfield,
|
|
149
|
+
portrait: { x: 25, y: 40, width: 50, height: 15 },
|
|
150
|
+
landscape: { x: 40, y: 25, width: 15, height: 50 },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'RM',
|
|
154
|
+
label: 'RM',
|
|
155
|
+
color: colors.midfield,
|
|
156
|
+
portrait: { x: 75, y: 40, width: 25, height: 15 },
|
|
157
|
+
landscape: { x: 40, y: 75, width: 15, height: 25 },
|
|
158
|
+
},
|
|
159
|
+
// ============ ATTACKING ZONE ============
|
|
160
|
+
{
|
|
161
|
+
id: 'LW',
|
|
162
|
+
label: 'LW',
|
|
163
|
+
color: colors.forward,
|
|
164
|
+
portrait: { x: 0, y: 0, width: 25, height: 40 },
|
|
165
|
+
landscape: { x: 0, y: 0, width: 40, height: 25 },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'CAM',
|
|
169
|
+
label: 'CAM',
|
|
170
|
+
color: colors.attackingMid,
|
|
171
|
+
portrait: { x: 25, y: 25, width: 50, height: 15 },
|
|
172
|
+
landscape: { x: 25, y: 25, width: 15, height: 50 },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'RW',
|
|
176
|
+
label: 'RW',
|
|
177
|
+
color: colors.forward,
|
|
178
|
+
portrait: { x: 75, y: 0, width: 25, height: 40 },
|
|
179
|
+
landscape: { x: 0, y: 75, width: 40, height: 25 },
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'CF',
|
|
183
|
+
label: 'CF',
|
|
184
|
+
color: colors.forward,
|
|
185
|
+
portrait: { x: 25, y: 10, width: 50, height: 15 },
|
|
186
|
+
landscape: { x: 10, y: 25, width: 15, height: 50 },
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'ST',
|
|
190
|
+
label: 'ST',
|
|
191
|
+
color: colors.striker,
|
|
192
|
+
portrait: { x: 25, y: 0, width: 50, height: 10 },
|
|
193
|
+
landscape: { x: 0, y: 25, width: 10, height: 50 },
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class SoccerBoardPlayerComponent {
|
|
199
|
+
// Host element reference
|
|
200
|
+
hostElement = inject((ElementRef));
|
|
201
|
+
// Inputs using signals
|
|
202
|
+
player = input.required(...(ngDevMode ? [{ debugName: "player" }] : []));
|
|
203
|
+
size = input(SoccerBoardPlayerSize.Medium, ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
204
|
+
// Outputs
|
|
205
|
+
dragStart = output();
|
|
206
|
+
dragging = output();
|
|
207
|
+
dragEnd = output();
|
|
208
|
+
removeFromField = output();
|
|
209
|
+
playerClicked = output();
|
|
210
|
+
// Drag state signals
|
|
211
|
+
isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
|
|
212
|
+
dragOffsetX = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetX" }] : []));
|
|
213
|
+
dragOffsetY = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetY" }] : []));
|
|
214
|
+
// Computed drag styles
|
|
215
|
+
dragStyles = computed(() => {
|
|
216
|
+
if (this.isDragging()) {
|
|
217
|
+
return {
|
|
218
|
+
opacity: '0.7',
|
|
219
|
+
cursor: 'grabbing',
|
|
220
|
+
zIndex: '1000',
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
opacity: '1',
|
|
225
|
+
cursor: 'grab',
|
|
226
|
+
zIndex: '',
|
|
227
|
+
};
|
|
228
|
+
}, ...(ngDevMode ? [{ debugName: "dragStyles" }] : []));
|
|
229
|
+
// Computed values
|
|
230
|
+
playerPhoto = computed(() => {
|
|
231
|
+
const photo = this.player()?.photo;
|
|
232
|
+
return photo || null;
|
|
233
|
+
}, ...(ngDevMode ? [{ debugName: "playerPhoto" }] : []));
|
|
234
|
+
playerName = computed(() => this.player()?.name || '', ...(ngDevMode ? [{ debugName: "playerName" }] : []));
|
|
235
|
+
playerNumber = computed(() => this.player()?.number, ...(ngDevMode ? [{ debugName: "playerNumber" }] : []));
|
|
236
|
+
// Determine if player is on the field (has fieldX and fieldY coordinates)
|
|
237
|
+
isOnField = computed(() => {
|
|
238
|
+
const player = this.player();
|
|
239
|
+
return player?.fieldX !== undefined && player?.fieldY !== undefined;
|
|
240
|
+
}, ...(ngDevMode ? [{ debugName: "isOnField" }] : []));
|
|
241
|
+
// Show fieldPosition if on field, otherwise show preferredPosition
|
|
242
|
+
playerPosition = computed(() => {
|
|
243
|
+
const player = this.player();
|
|
244
|
+
if (this.isOnField() && player?.fieldPosition) {
|
|
245
|
+
return player.fieldPosition;
|
|
246
|
+
}
|
|
247
|
+
return player?.preferredPosition;
|
|
248
|
+
}, ...(ngDevMode ? [{ debugName: "playerPosition" }] : []));
|
|
249
|
+
// Size classes
|
|
250
|
+
sizeClasses = computed(() => {
|
|
251
|
+
const size = this.size();
|
|
252
|
+
switch (size) {
|
|
253
|
+
case SoccerBoardPlayerSize.Small:
|
|
254
|
+
return 'w-12 h-12';
|
|
255
|
+
case SoccerBoardPlayerSize.Large:
|
|
256
|
+
return 'w-20 h-20';
|
|
257
|
+
default:
|
|
258
|
+
return 'w-16 h-16';
|
|
259
|
+
}
|
|
260
|
+
}, ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
|
|
261
|
+
constructor() {
|
|
262
|
+
// Effect to apply drag styles
|
|
263
|
+
effect(() => {
|
|
264
|
+
const styles = this.dragStyles();
|
|
265
|
+
const element = this.hostElement.nativeElement;
|
|
266
|
+
element.style.opacity = styles.opacity;
|
|
267
|
+
element.style.cursor = styles.cursor;
|
|
268
|
+
element.style.zIndex = styles.zIndex;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
onMouseDown(event) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
this.startDrag(event.clientX, event.clientY);
|
|
274
|
+
}
|
|
275
|
+
onTouchStart(event) {
|
|
276
|
+
if (event.touches.length !== 1)
|
|
277
|
+
return;
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
const touch = event.touches[0];
|
|
280
|
+
this.startDrag(touch.clientX, touch.clientY);
|
|
281
|
+
}
|
|
282
|
+
startDrag(clientX, clientY) {
|
|
283
|
+
console.log('[Player] Start drag');
|
|
284
|
+
const element = this.hostElement.nativeElement;
|
|
285
|
+
const rect = element.getBoundingClientRect();
|
|
286
|
+
this.dragOffsetX.set(clientX - rect.left);
|
|
287
|
+
this.dragOffsetY.set(clientY - rect.top);
|
|
288
|
+
this.isDragging.set(true);
|
|
289
|
+
const player = this.player();
|
|
290
|
+
console.log('[Player] Emitting dragStart', { playerId: player.id, playerName: player.name });
|
|
291
|
+
this.dragStart.emit({
|
|
292
|
+
playerId: player.id,
|
|
293
|
+
playerName: player.name,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
onMouseMove(event) {
|
|
297
|
+
if (!this.isDragging())
|
|
298
|
+
return;
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
this.handleDrag(event.clientX, event.clientY);
|
|
301
|
+
}
|
|
302
|
+
onTouchMove(event) {
|
|
303
|
+
if (!this.isDragging() || event.touches.length !== 1)
|
|
304
|
+
return;
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
const touch = event.touches[0];
|
|
307
|
+
this.handleDrag(touch.clientX, touch.clientY);
|
|
308
|
+
}
|
|
309
|
+
handleDrag(clientX, clientY) {
|
|
310
|
+
const element = this.hostElement.nativeElement;
|
|
311
|
+
const offsetX = this.dragOffsetX();
|
|
312
|
+
const offsetY = this.dragOffsetY();
|
|
313
|
+
// Move visually during drag
|
|
314
|
+
element.style.position = 'fixed';
|
|
315
|
+
element.style.left = `${clientX - offsetX}px`;
|
|
316
|
+
element.style.top = `${clientY - offsetY}px`;
|
|
317
|
+
element.style.pointerEvents = 'none';
|
|
318
|
+
const player = this.player();
|
|
319
|
+
this.dragging.emit({
|
|
320
|
+
playerId: player.id,
|
|
321
|
+
clientX,
|
|
322
|
+
clientY,
|
|
323
|
+
offsetX,
|
|
324
|
+
offsetY,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
onMouseUp(event) {
|
|
328
|
+
if (!this.isDragging())
|
|
329
|
+
return;
|
|
330
|
+
this.endDrag(event.clientX, event.clientY);
|
|
331
|
+
}
|
|
332
|
+
onTouchEnd(event) {
|
|
333
|
+
if (!this.isDragging())
|
|
334
|
+
return;
|
|
335
|
+
const touch = event.changedTouches[0];
|
|
336
|
+
this.endDrag(touch.clientX, touch.clientY);
|
|
337
|
+
}
|
|
338
|
+
endDrag(clientX, clientY) {
|
|
339
|
+
console.log('[Player] End drag', { clientX, clientY });
|
|
340
|
+
this.isDragging.set(false);
|
|
341
|
+
const element = this.hostElement.nativeElement;
|
|
342
|
+
element.style.pointerEvents = '';
|
|
343
|
+
element.style.position = '';
|
|
344
|
+
element.style.left = '';
|
|
345
|
+
element.style.top = '';
|
|
346
|
+
const player = this.player();
|
|
347
|
+
const dragEndEvent = {
|
|
348
|
+
playerId: player.id,
|
|
349
|
+
clientX,
|
|
350
|
+
clientY,
|
|
351
|
+
offsetX: this.dragOffsetX(),
|
|
352
|
+
offsetY: this.dragOffsetY(),
|
|
353
|
+
};
|
|
354
|
+
console.log('[Player] Emitting dragEnd', dragEndEvent);
|
|
355
|
+
this.dragEnd.emit(dragEndEvent);
|
|
356
|
+
}
|
|
357
|
+
onRemoveClick(event) {
|
|
358
|
+
event.stopPropagation();
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
const player = this.player();
|
|
361
|
+
console.log('[Player] Remove from field clicked', player.id);
|
|
362
|
+
this.removeFromField.emit({ playerId: player.id });
|
|
363
|
+
}
|
|
364
|
+
onPlayerClick(event) {
|
|
365
|
+
// Don't emit click if we're dragging or if the click was on the remove button
|
|
366
|
+
if (this.isDragging()) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
event.stopPropagation();
|
|
370
|
+
const player = this.player();
|
|
371
|
+
console.log('[Player] Player clicked', player.id);
|
|
372
|
+
this.playerClicked.emit({ playerId: player.id, player });
|
|
373
|
+
}
|
|
374
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
375
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardPlayerComponent, isStandalone: true, selector: "lib-soccer-board-player", inputs: { player: { classPropertyName: "player", publicName: "player", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragStart: "dragStart", dragging: "dragging", dragEnd: "dragEnd", removeFromField: "removeFromField", playerClicked: "playerClicked" }, host: { listeners: { "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)", "document:mousemove": "onMouseMove($event)", "document:touchmove": "onTouchMove($event)", "document:mouseup": "onMouseUp($event)", "document:touchend": "onTouchEnd($event)" } }, ngImport: i0, template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] });
|
|
376
|
+
}
|
|
377
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, decorators: [{
|
|
378
|
+
type: Component,
|
|
379
|
+
args: [{ selector: 'lib-soccer-board-player', standalone: true, imports: [], template: "<div\n class=\"player-card group select-none cursor-pointer\"\n [class]=\"sizeClasses()\"\n (click)=\"onPlayerClick($event)\"\n>\n <!-- Golden Border Wrapper -->\n <div class=\"player-border-wrapper\">\n <!-- Main Content Container -->\n <div class=\"player-content relative w-full h-full overflow-hidden rounded-md shadow-lg\">\n <!-- Top Left: Number and Position - Above the photo -->\n <div class=\"player-info-top-left absolute left-0.5 top-1 flex flex-col gap-0.5 z-30\">\n <!-- Player Number -->\n @if (playerNumber()) {\n <div class=\"player-number-badge text-white text-xs font-bold\">\n {{ playerNumber() }}\n </div>\n }\n\n <!-- Position -->\n @if (playerPosition()) {\n <div class=\"player-position-badge text-white text-[10px] font-bold\">\n {{ playerPosition() }}\n </div>\n }\n </div>\n\n <!-- Right Side: Player Photo -->\n <div class=\"player-photo-container absolute right-0 bottom-0 w-4/5 overflow-visible\">\n @if (playerPhoto()) {\n <img\n [src]=\"playerPhoto()!\"\n [alt]=\"playerName()\"\n class=\"player-photo-image\"\n />\n } @else {\n <!-- Placeholder -->\n <div class=\"w-full h-full flex items-center justify-center bg-gradient-to-br from-gray-400 to-gray-600\">\n <svg class=\"w-8 h-8 text-white opacity-50\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path fill-rule=\"evenodd\" d=\"M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n }\n </div>\n\n <!-- Hover Effect Overlay -->\n <div class=\"absolute inset-0 bg-white/0 group-hover:bg-white/10 transition-all duration-200 rounded-md pointer-events-none\"></div>\n </div>\n </div>\n\n <!-- Remove Button (only when on field) - Outside the card -->\n @if (isOnField()) {\n <button\n class=\"remove-badge absolute bg-red-500 hover:bg-red-600 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center shadow-lg transition-colors z-30 border-2 border-white\"\n (click)=\"onRemoveClick($event)\"\n type=\"button\"\n aria-label=\"Remove from field\"\n >\n \u00D7\n </button>\n }\n\n <!-- Player Name (outside the card, below) -->\n <div class=\"player-name-outside text-white text-[10px] font-semibold truncate text-center leading-tight mt-0.5\">\n {{ playerName() }}\n </div>\n</div>\n", styles: [":host{display:inline-block;position:relative;cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}.player-card{position:relative;transition:transform .2s ease,box-shadow .2s ease}.player-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.player-card:active{transform:scale(.95)}.player-border-wrapper{position:relative;width:100%;height:100%;padding:2px;border-radius:8px;background:linear-gradient(135deg,#d4af37,#f4d03f,#d4af37,#f4d03f,#d4af37);background-size:200% 200%;animation:shimmer 3s ease-in-out infinite;box-shadow:0 0 8px #d4af3780}.player-content{position:relative;width:100%;height:100%;border-radius:6px;background:repeating-linear-gradient(45deg,#16a34a 0px 2px,#22c55e 2px 4px,#16a34a 4px 6px,#15803d 6px 8px),linear-gradient(135deg,#15803d,#16a34a,#22c55e,#16a34a,#15803d);background-size:20px 20px,100% 100%;background-blend-mode:overlay}@keyframes shimmer{0%,to{background-position:0% 50%}50%{background-position:100% 50%}}.player-info-top-left{z-index:30;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5);background:linear-gradient(to right,rgba(0,0,0,.3) 0%,transparent 50%);padding-right:.5rem;border-radius:0 .25rem .25rem 0}.player-number-badge{font-size:12px;font-weight:900;line-height:1;letter-spacing:-.5px}.player-position-badge{font-size:9px;font-weight:700;line-height:1;letter-spacing:.5px}.remove-badge{box-shadow:0 2px 8px #0006;top:-6px;right:-6px}.player-photo-container{height:100%;width:85%;display:flex;align-items:flex-end;justify-content:flex-start;z-index:1;overflow:visible}.player-photo-image{width:100%;height:auto;max-height:140%;object-fit:contain;object-position:center bottom;display:block}.player-name-outside{width:100%;text-shadow:1px 1px 2px rgba(0,0,0,.8),0 0 4px rgba(0,0,0,.5)}:host-context(.w-12) .player-info-top-left{left:.125rem;top:.25rem;gap:.125rem}:host-context(.w-12) .player-number-badge{font-size:9px}:host-context(.w-12) .player-position-badge{font-size:7px}:host-context(.w-12) .player-name-outside{font-size:8px}:host-context(.w-12) .remove-badge{width:18px;height:18px;font-size:10px;top:-2px;right:-2px}:host-context(.w-20) .player-info-top-left{left:.375rem;top:.5rem;gap:.25rem}:host-context(.w-20) .player-number-badge{font-size:16px}:host-context(.w-20) .player-position-badge{font-size:11px}:host-context(.w-20) .player-name-outside{font-size:12px}:host-context(.w-20) .remove-badge{width:24px;height:24px;font-size:14px;top:-3px;right:-3px}\n"] }]
|
|
380
|
+
}], ctorParameters: () => [], propDecorators: { player: [{ type: i0.Input, args: [{ isSignal: true, alias: "player", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragging: [{ type: i0.Output, args: ["dragging"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }], removeFromField: [{ type: i0.Output, args: ["removeFromField"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], onMouseDown: [{
|
|
381
|
+
type: HostListener,
|
|
382
|
+
args: ['mousedown', ['$event']]
|
|
383
|
+
}], onTouchStart: [{
|
|
384
|
+
type: HostListener,
|
|
385
|
+
args: ['touchstart', ['$event']]
|
|
386
|
+
}], onMouseMove: [{
|
|
387
|
+
type: HostListener,
|
|
388
|
+
args: ['document:mousemove', ['$event']]
|
|
389
|
+
}], onTouchMove: [{
|
|
390
|
+
type: HostListener,
|
|
391
|
+
args: ['document:touchmove', ['$event']]
|
|
392
|
+
}], onMouseUp: [{
|
|
393
|
+
type: HostListener,
|
|
394
|
+
args: ['document:mouseup', ['$event']]
|
|
395
|
+
}], onTouchEnd: [{
|
|
396
|
+
type: HostListener,
|
|
397
|
+
args: ['document:touchend', ['$event']]
|
|
398
|
+
}] } });
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Soccer Board Component
|
|
402
|
+
* Renders a FIFA-compliant soccer field with two halves (HOME and AWAY)
|
|
403
|
+
* Supports portrait and landscape orientations
|
|
404
|
+
*/
|
|
405
|
+
class SoccerBoardComponent {
|
|
406
|
+
// Host element reference
|
|
407
|
+
hostElement = inject((ElementRef));
|
|
408
|
+
platformId = inject(PLATFORM_ID);
|
|
409
|
+
// Expose SoccerBoardPlayerSize to template
|
|
410
|
+
PlayerSize = SoccerBoardPlayerSize;
|
|
411
|
+
// Inputs using signals
|
|
412
|
+
orientation = input(SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
|
|
413
|
+
teamSide = input(null, ...(ngDevMode ? [{ debugName: "teamSide" }] : []));
|
|
414
|
+
showPositions = input(false, ...(ngDevMode ? [{ debugName: "showPositions" }] : []));
|
|
415
|
+
fit = input(null, ...(ngDevMode ? [{ debugName: "fit" }] : []));
|
|
416
|
+
players = input([], ...(ngDevMode ? [{ debugName: "players" }] : []));
|
|
417
|
+
// Layout configuration
|
|
418
|
+
showPlayersSidebar = input(true, ...(ngDevMode ? [{ debugName: "showPlayersSidebar" }] : []));
|
|
419
|
+
playersPosition = input('left', ...(ngDevMode ? [{ debugName: "playersPosition" }] : []));
|
|
420
|
+
playersColumns = input(2, ...(ngDevMode ? [{ debugName: "playersColumns" }] : []));
|
|
421
|
+
// Maximum players per side (default: 7)
|
|
422
|
+
maxPlayersPerSide = input(7, ...(ngDevMode ? [{ debugName: "maxPlayersPerSide" }] : []));
|
|
423
|
+
// ViewChild references
|
|
424
|
+
fieldContainer = viewChild('fieldContainer', ...(ngDevMode ? [{ debugName: "fieldContainer" }] : []));
|
|
425
|
+
homeHalf = viewChild('homeHalf', ...(ngDevMode ? [{ debugName: "homeHalf" }] : []));
|
|
426
|
+
awayHalf = viewChild('awayHalf', ...(ngDevMode ? [{ debugName: "awayHalf" }] : []));
|
|
427
|
+
soccerBoardContainer = viewChild('soccerBoardContainer', ...(ngDevMode ? [{ debugName: "soccerBoardContainer" }] : []));
|
|
428
|
+
// Outputs
|
|
429
|
+
playerPositioned = output();
|
|
430
|
+
playerRemovedFromField = output();
|
|
431
|
+
imageExported = output();
|
|
432
|
+
playerClicked = output();
|
|
433
|
+
// Method to handle player drag end (called from template)
|
|
434
|
+
onPlayerDragEnd(event) {
|
|
435
|
+
this.handlePlayerDrop(event);
|
|
436
|
+
}
|
|
437
|
+
// Method to handle player click (called from template)
|
|
438
|
+
onPlayerClick(event) {
|
|
439
|
+
console.log('[SoccerBoard] Player clicked', event.playerId);
|
|
440
|
+
this.playerClicked.emit(event);
|
|
441
|
+
}
|
|
442
|
+
// Method to handle player remove from field (called from template)
|
|
443
|
+
onPlayerRemoveFromField(event) {
|
|
444
|
+
// Emit event to parent to handle removal
|
|
445
|
+
console.log('[SoccerBoard] Player remove requested', event.playerId);
|
|
446
|
+
this.playerRemovedFromField.emit({ playerId: event.playerId });
|
|
447
|
+
}
|
|
448
|
+
// Reactive width signal for host element
|
|
449
|
+
hostWidth = signal('', ...(ngDevMode ? [{ debugName: "hostWidth" }] : []));
|
|
450
|
+
// Host binding for width (reactive)
|
|
451
|
+
get hostWidthStyle() {
|
|
452
|
+
return this.hostWidth();
|
|
453
|
+
}
|
|
454
|
+
// Computed values
|
|
455
|
+
isLandscape = computed(() => this.orientation() === SoccerBoardOrientation.Landscape, ...(ngDevMode ? [{ debugName: "isLandscape" }] : []));
|
|
456
|
+
showHome = computed(() => {
|
|
457
|
+
const side = this.teamSide();
|
|
458
|
+
return side === null || side === SoccerBoardTeamSide.Home;
|
|
459
|
+
}, ...(ngDevMode ? [{ debugName: "showHome" }] : []));
|
|
460
|
+
showAway = computed(() => {
|
|
461
|
+
const side = this.teamSide();
|
|
462
|
+
return side === null || side === SoccerBoardTeamSide.Away;
|
|
463
|
+
}, ...(ngDevMode ? [{ debugName: "showAway" }] : []));
|
|
464
|
+
// Computed position zones
|
|
465
|
+
homeZones = computed(() => {
|
|
466
|
+
if (!this.showPositions()) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const positionsData = getPositionsData();
|
|
470
|
+
const orientation = this.orientation();
|
|
471
|
+
return positionsData.map((pos) => ({
|
|
472
|
+
...pos,
|
|
473
|
+
coords: pos[orientation],
|
|
474
|
+
}));
|
|
475
|
+
}, ...(ngDevMode ? [{ debugName: "homeZones" }] : []));
|
|
476
|
+
awayZones = computed(() => {
|
|
477
|
+
if (!this.showPositions()) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
const positionsData = getPositionsData();
|
|
481
|
+
const orientation = this.orientation();
|
|
482
|
+
return positionsData.map((pos) => {
|
|
483
|
+
const coords = pos[orientation];
|
|
484
|
+
let awayCoords;
|
|
485
|
+
if (orientation === SoccerBoardOrientation.Portrait) {
|
|
486
|
+
// Portrait: invert Y
|
|
487
|
+
awayCoords = {
|
|
488
|
+
...coords,
|
|
489
|
+
y: 100 - coords.y - coords.height,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
// Landscape: invert X
|
|
494
|
+
awayCoords = {
|
|
495
|
+
...coords,
|
|
496
|
+
x: 100 - coords.x - coords.width,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
...pos,
|
|
501
|
+
coords: awayCoords,
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}, ...(ngDevMode ? [{ debugName: "awayZones" }] : []));
|
|
505
|
+
// Computed players by side
|
|
506
|
+
homePlayers = computed(() => {
|
|
507
|
+
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home && player.fieldX !== undefined && player.fieldY !== undefined);
|
|
508
|
+
}, ...(ngDevMode ? [{ debugName: "homePlayers" }] : []));
|
|
509
|
+
awayPlayers = computed(() => {
|
|
510
|
+
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away && player.fieldX !== undefined && player.fieldY !== undefined);
|
|
511
|
+
}, ...(ngDevMode ? [{ debugName: "awayPlayers" }] : []));
|
|
512
|
+
// Computed: players available (not on field)
|
|
513
|
+
availablePlayers = computed(() => {
|
|
514
|
+
return this.players().filter((player) => player.fieldX === undefined || player.fieldY === undefined);
|
|
515
|
+
}, ...(ngDevMode ? [{ debugName: "availablePlayers" }] : []));
|
|
516
|
+
// Computed: layout classes based on players position
|
|
517
|
+
layoutClasses = computed(() => {
|
|
518
|
+
if (!this.showPlayersSidebar()) {
|
|
519
|
+
return 'layout-no-sidebar';
|
|
520
|
+
}
|
|
521
|
+
const position = this.playersPosition();
|
|
522
|
+
switch (position) {
|
|
523
|
+
case 'right':
|
|
524
|
+
return 'layout-players-right';
|
|
525
|
+
case 'top':
|
|
526
|
+
return 'layout-players-top';
|
|
527
|
+
case 'bottom':
|
|
528
|
+
return 'layout-players-bottom';
|
|
529
|
+
default:
|
|
530
|
+
return 'layout-players-left';
|
|
531
|
+
}
|
|
532
|
+
}, ...(ngDevMode ? [{ debugName: "layoutClasses" }] : []));
|
|
533
|
+
// Computed: players grid columns
|
|
534
|
+
playersGridColumns = computed(() => {
|
|
535
|
+
return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
|
|
536
|
+
}, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
|
|
537
|
+
// Helper method to get visual position for a player (public for template)
|
|
538
|
+
getPlayerPosition(player) {
|
|
539
|
+
if (player.fieldX === undefined || player.fieldY === undefined || !player.side) {
|
|
540
|
+
return { left: 0, top: 0 };
|
|
541
|
+
}
|
|
542
|
+
return this.fieldToHalfCoordinates(player.fieldX, player.fieldY, player.side);
|
|
543
|
+
}
|
|
544
|
+
resizeObserver = null;
|
|
545
|
+
constructor() {
|
|
546
|
+
// Only run effects in browser (SSR protection)
|
|
547
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
548
|
+
// Effect to update field size when fit or orientation changes
|
|
549
|
+
effect(() => {
|
|
550
|
+
this.fit();
|
|
551
|
+
this.orientation();
|
|
552
|
+
// Use setTimeout to ensure view is initialized
|
|
553
|
+
setTimeout(() => {
|
|
554
|
+
this.setupResizeObserver();
|
|
555
|
+
this.updateFieldSize();
|
|
556
|
+
}, 0);
|
|
557
|
+
});
|
|
558
|
+
// Effect to watch for --soccer-board-width CSS custom property changes
|
|
559
|
+
effect(() => {
|
|
560
|
+
// Trigger update when orientation changes (affects width calculation)
|
|
561
|
+
this.orientation();
|
|
562
|
+
if (this.fit() !== SoccerBoardFitMode.Contain) {
|
|
563
|
+
this.updateFieldSizeFromCSS();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Handles player drag end event
|
|
570
|
+
* Called from parent component when a player is dropped
|
|
571
|
+
* @public
|
|
572
|
+
*/
|
|
573
|
+
handlePlayerDrop(event) {
|
|
574
|
+
console.log('[SoccerBoard] handlePlayerDrop called', event);
|
|
575
|
+
const position = this.calculateFieldPosition(event.clientX, event.clientY);
|
|
576
|
+
console.log('[SoccerBoard] Calculated position', position);
|
|
577
|
+
if (position) {
|
|
578
|
+
// Check if player is already on the field (moving to a different position)
|
|
579
|
+
const currentPlayer = this.players().find((p) => p.id === event.playerId);
|
|
580
|
+
const isMovingPlayer = currentPlayer?.fieldX !== undefined && currentPlayer?.fieldY !== undefined;
|
|
581
|
+
// Check maximum players per side (only for new players, not when moving)
|
|
582
|
+
if (!isMovingPlayer) {
|
|
583
|
+
const playersOnSide = this.players().filter((p) => p.side === position.side && p.fieldX !== undefined && p.fieldY !== undefined).length;
|
|
584
|
+
if (playersOnSide >= this.maxPlayersPerSide()) {
|
|
585
|
+
console.warn(`[SoccerBoard] Maximum players (${this.maxPlayersPerSide()}) reached for ${position.side} side`);
|
|
586
|
+
return; // Don't allow drop if max players reached
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const tacticalPosition = this.detectTacticalPosition(position.fieldX, position.fieldY, position.side);
|
|
590
|
+
console.log('🎯 Player dropped:', {
|
|
591
|
+
playerId: event.playerId,
|
|
592
|
+
position: tacticalPosition,
|
|
593
|
+
coordinates: {
|
|
594
|
+
fieldX: position.fieldX,
|
|
595
|
+
fieldY: position.fieldY,
|
|
596
|
+
side: position.side,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
// Emit event with position information
|
|
600
|
+
this.playerPositioned.emit({
|
|
601
|
+
playerId: event.playerId,
|
|
602
|
+
fieldX: position.fieldX,
|
|
603
|
+
fieldY: position.fieldY,
|
|
604
|
+
side: position.side,
|
|
605
|
+
position: tacticalPosition,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.warn('[SoccerBoard] Position is null - player not dropped on field');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Calculates field position from client coordinates
|
|
614
|
+
*/
|
|
615
|
+
calculateFieldPosition(clientX, clientY) {
|
|
616
|
+
const homeHalfEl = this.homeHalf()?.nativeElement;
|
|
617
|
+
const awayHalfEl = this.awayHalf()?.nativeElement;
|
|
618
|
+
if (!homeHalfEl || !awayHalfEl) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
// Get the center point of the dragged element
|
|
622
|
+
// Use clientX/clientY directly as they represent where the mouse/touch is
|
|
623
|
+
// The offset is already accounted for in the drag handling
|
|
624
|
+
const centerX = clientX;
|
|
625
|
+
const centerY = clientY;
|
|
626
|
+
// Check HOME half
|
|
627
|
+
const homeRect = homeHalfEl.getBoundingClientRect();
|
|
628
|
+
const isInHome = centerX >= homeRect.left &&
|
|
629
|
+
centerX <= homeRect.right &&
|
|
630
|
+
centerY >= homeRect.top &&
|
|
631
|
+
centerY <= homeRect.bottom;
|
|
632
|
+
// Check AWAY half
|
|
633
|
+
const awayRect = awayHalfEl.getBoundingClientRect();
|
|
634
|
+
const isInAway = centerX >= awayRect.left &&
|
|
635
|
+
centerX <= awayRect.right &&
|
|
636
|
+
centerY >= awayRect.top &&
|
|
637
|
+
centerY <= awayRect.bottom;
|
|
638
|
+
if (!isInHome && !isInAway) {
|
|
639
|
+
return null; // Not over the field
|
|
640
|
+
}
|
|
641
|
+
const side = isInHome ? SoccerBoardTeamSide.Home : SoccerBoardTeamSide.Away;
|
|
642
|
+
const halfRect = isInHome ? homeRect : awayRect;
|
|
643
|
+
// Calculate position within the half (0-100%)
|
|
644
|
+
const relativeX = ((centerX - halfRect.left) / halfRect.width) * 100;
|
|
645
|
+
const relativeY = ((centerY - halfRect.top) / halfRect.height) * 100;
|
|
646
|
+
// Clamp to 0-100%
|
|
647
|
+
const clampedX = Math.max(0, Math.min(100, relativeX));
|
|
648
|
+
const clampedY = Math.max(0, Math.min(100, relativeY));
|
|
649
|
+
// Convert to field coordinates (normalized)
|
|
650
|
+
const orientation = this.orientation();
|
|
651
|
+
let fieldX;
|
|
652
|
+
let fieldY;
|
|
653
|
+
if (orientation === SoccerBoardOrientation.Portrait) {
|
|
654
|
+
// Portrait: X is width, Y is length
|
|
655
|
+
fieldX = clampedX;
|
|
656
|
+
if (side === SoccerBoardTeamSide.Home) {
|
|
657
|
+
// HOME is bottom half (50-100% of field length)
|
|
658
|
+
fieldY = 50 + clampedY / 2;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// AWAY is top half (0-50% of field length)
|
|
662
|
+
fieldY = clampedY / 2;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
// Landscape: X is length, Y is width
|
|
667
|
+
fieldY = clampedY; // Y is the width (top-bottom in visual space)
|
|
668
|
+
if (side === SoccerBoardTeamSide.Home) {
|
|
669
|
+
// HOME is right half (50-100% of field length)
|
|
670
|
+
fieldX = 50 + clampedX / 2;
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// AWAY is left half (0-50% of field length)
|
|
674
|
+
fieldX = clampedX / 2;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { fieldX, fieldY, side };
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Converts field coordinates to half coordinates
|
|
681
|
+
*/
|
|
682
|
+
fieldToHalfCoordinates(fieldX, fieldY, side) {
|
|
683
|
+
const orientation = this.orientation();
|
|
684
|
+
let left;
|
|
685
|
+
let top;
|
|
686
|
+
if (orientation === SoccerBoardOrientation.Portrait) {
|
|
687
|
+
// Portrait mode
|
|
688
|
+
left = fieldX;
|
|
689
|
+
if (side === SoccerBoardTeamSide.Home) {
|
|
690
|
+
// HOME: fieldY 50-100% maps to top 0-100%
|
|
691
|
+
top = (fieldY - 50) * 2;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
// AWAY: fieldY 0-50% maps to top 0-100%
|
|
695
|
+
top = fieldY * 2;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
// Landscape mode
|
|
700
|
+
top = fieldY;
|
|
701
|
+
if (side === SoccerBoardTeamSide.Home) {
|
|
702
|
+
// HOME: fieldX 50-100% maps to left 0-100%
|
|
703
|
+
left = (fieldX - 50) * 2;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// AWAY: fieldX 0-50% maps to left 0-100%
|
|
707
|
+
left = fieldX * 2;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return { left, top };
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Detects tactical position based on field coordinates
|
|
714
|
+
*/
|
|
715
|
+
detectTacticalPosition(fieldX, fieldY, side) {
|
|
716
|
+
// Convert field coordinates to half coordinates
|
|
717
|
+
const { left: halfX, top: halfY } = this.fieldToHalfCoordinates(fieldX, fieldY, side);
|
|
718
|
+
// Get position data (same as show-positions)
|
|
719
|
+
const positionsData = getPositionsData();
|
|
720
|
+
const orientation = this.orientation();
|
|
721
|
+
// Find which position zone contains this point
|
|
722
|
+
let detectedPosition = 'CM'; // Default fallback
|
|
723
|
+
let smallestArea = Infinity;
|
|
724
|
+
for (const pos of positionsData) {
|
|
725
|
+
let coords = pos[orientation];
|
|
726
|
+
// For AWAY, we need to invert coordinates (same logic as renderPositionZones)
|
|
727
|
+
if (side === SoccerBoardTeamSide.Away) {
|
|
728
|
+
if (orientation === SoccerBoardOrientation.Portrait) {
|
|
729
|
+
// Portrait: invert Y
|
|
730
|
+
coords = {
|
|
731
|
+
...coords,
|
|
732
|
+
y: 100 - coords.y - coords.height,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
// Landscape: invert X
|
|
737
|
+
coords = {
|
|
738
|
+
...coords,
|
|
739
|
+
x: 100 - coords.x - coords.width,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Check if point is inside this zone
|
|
744
|
+
const isInside = halfX >= coords.x &&
|
|
745
|
+
halfX <= coords.x + coords.width &&
|
|
746
|
+
halfY >= coords.y &&
|
|
747
|
+
halfY <= coords.y + coords.height;
|
|
748
|
+
if (isInside) {
|
|
749
|
+
// If multiple zones overlap, prefer the smallest one (most specific)
|
|
750
|
+
const area = coords.width * coords.height;
|
|
751
|
+
if (area < smallestArea) {
|
|
752
|
+
smallestArea = area;
|
|
753
|
+
detectedPosition = pos.id;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return detectedPosition;
|
|
758
|
+
}
|
|
759
|
+
ngAfterViewInit() {
|
|
760
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
// Initial setup
|
|
764
|
+
setTimeout(() => {
|
|
765
|
+
this.updateFieldSize();
|
|
766
|
+
this.setupResizeObserver();
|
|
767
|
+
}, 0);
|
|
768
|
+
}
|
|
769
|
+
ngOnDestroy() {
|
|
770
|
+
this.cleanupResizeObserver();
|
|
771
|
+
}
|
|
772
|
+
setupResizeObserver() {
|
|
773
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
this.cleanupResizeObserver();
|
|
777
|
+
if (this.fit() !== SoccerBoardFitMode.Contain) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
// Observe the host element's parent (like in the original)
|
|
781
|
+
const parentElement = this.hostElement.nativeElement.parentElement;
|
|
782
|
+
if (!parentElement) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
786
|
+
this.updateFieldSize();
|
|
787
|
+
});
|
|
788
|
+
this.resizeObserver.observe(parentElement);
|
|
789
|
+
}
|
|
790
|
+
cleanupResizeObserver() {
|
|
791
|
+
if (this.resizeObserver) {
|
|
792
|
+
this.resizeObserver.disconnect();
|
|
793
|
+
this.resizeObserver = null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
updateFieldSize() {
|
|
797
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const hostElement = this.hostElement.nativeElement;
|
|
801
|
+
// If fit="contain", calculate size based on parent container (like original)
|
|
802
|
+
if (this.fit() === SoccerBoardFitMode.Contain && hostElement.parentElement) {
|
|
803
|
+
const parent = hostElement.parentElement;
|
|
804
|
+
const parentStyle = getComputedStyle(parent);
|
|
805
|
+
const parentWidth = parent.clientWidth -
|
|
806
|
+
parseFloat(parentStyle.paddingLeft) -
|
|
807
|
+
parseFloat(parentStyle.paddingRight);
|
|
808
|
+
const parentHeight = parent.clientHeight -
|
|
809
|
+
parseFloat(parentStyle.paddingTop) -
|
|
810
|
+
parseFloat(parentStyle.paddingBottom);
|
|
811
|
+
if (parentWidth > 0 && parentHeight > 0) {
|
|
812
|
+
const fieldAspectRatio = this.orientation() === SoccerBoardOrientation.Landscape
|
|
813
|
+
? SoccerBoardFieldConstants.RATIO
|
|
814
|
+
: 1 / SoccerBoardFieldConstants.RATIO;
|
|
815
|
+
const containerAspectRatio = parentWidth / parentHeight;
|
|
816
|
+
let width;
|
|
817
|
+
if (containerAspectRatio > fieldAspectRatio) {
|
|
818
|
+
// Container is wider - height is the constraint
|
|
819
|
+
const height = parentHeight;
|
|
820
|
+
width = height * fieldAspectRatio;
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
// Container is taller - width is the constraint
|
|
824
|
+
width = parentWidth;
|
|
825
|
+
}
|
|
826
|
+
// Update width signal reactively
|
|
827
|
+
this.hostWidth.set(`${width}px`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// Otherwise, use --soccer-board-width CSS custom property (like original)
|
|
832
|
+
this.updateFieldSizeFromCSS();
|
|
833
|
+
}
|
|
834
|
+
updateFieldSizeFromCSS() {
|
|
835
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const hostElement = this.hostElement.nativeElement;
|
|
839
|
+
const computedStyle = getComputedStyle(hostElement);
|
|
840
|
+
const fieldWidth = computedStyle.getPropertyValue('--soccer-board-width').trim();
|
|
841
|
+
if (!fieldWidth || fieldWidth === '100%') {
|
|
842
|
+
// Default behavior: 100% width, let CSS handle it
|
|
843
|
+
this.hostWidth.set(fieldWidth || '100%');
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
// Parse the width value
|
|
847
|
+
const match = fieldWidth.match(/^([\d.]+)(px|rem|em|vw|vh|%)?$/);
|
|
848
|
+
if (!match) {
|
|
849
|
+
this.hostWidth.set(fieldWidth);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const value = parseFloat(match[1]);
|
|
853
|
+
const unit = match[2] || 'px';
|
|
854
|
+
if (this.orientation() === SoccerBoardOrientation.Landscape) {
|
|
855
|
+
// In landscape, the CSS width represents the field length (105m)
|
|
856
|
+
// So we multiply the physical width (68m) by the ratio to get the CSS width
|
|
857
|
+
const landscapeWidth = value * SoccerBoardFieldConstants.RATIO;
|
|
858
|
+
this.hostWidth.set(`${landscapeWidth}${unit}`);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// In portrait, the CSS width represents the field width (68m)
|
|
862
|
+
this.hostWidth.set(`${value}${unit}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Exports the soccer board field (without sidebar) to PNG image
|
|
867
|
+
* @public
|
|
868
|
+
*/
|
|
869
|
+
async exportToPNG() {
|
|
870
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
871
|
+
console.warn('[SoccerBoard] Export to PNG is only available in browser');
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const container = this.fieldContainer()?.nativeElement;
|
|
875
|
+
if (!container) {
|
|
876
|
+
console.error('[SoccerBoard] Field container not found for export');
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
console.log('[SoccerBoard] Starting PNG export...');
|
|
881
|
+
// Wait for all images to load before exporting
|
|
882
|
+
const images = container.querySelectorAll('img');
|
|
883
|
+
const imagePromises = Array.from(images).map((img) => {
|
|
884
|
+
if (img.complete) {
|
|
885
|
+
return Promise.resolve();
|
|
886
|
+
}
|
|
887
|
+
return new Promise((resolve) => {
|
|
888
|
+
img.onload = () => resolve();
|
|
889
|
+
img.onerror = () => resolve(); // Continue even if image fails to load
|
|
890
|
+
// Timeout after 5 seconds
|
|
891
|
+
setTimeout(() => resolve(), 5000);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
await Promise.all(imagePromises);
|
|
895
|
+
console.log('[SoccerBoard] All images loaded, capturing...');
|
|
896
|
+
// Use html2canvas-pro to capture the field (supports oklab/oklch colors)
|
|
897
|
+
const canvas = await html2canvas(container, {
|
|
898
|
+
backgroundColor: null, // Transparent background
|
|
899
|
+
scale: 2, // Higher quality
|
|
900
|
+
logging: false,
|
|
901
|
+
useCORS: true, // Allow cross-origin images
|
|
902
|
+
allowTaint: false,
|
|
903
|
+
width: container.offsetWidth,
|
|
904
|
+
height: container.offsetHeight,
|
|
905
|
+
ignoreElements: (element) => {
|
|
906
|
+
// Ignore remove badges
|
|
907
|
+
return element.classList?.contains('remove-badge') || false;
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
// Convert canvas to blob and emit event
|
|
911
|
+
canvas.toBlob((blob) => {
|
|
912
|
+
if (!blob) {
|
|
913
|
+
console.error('[SoccerBoard] Failed to create blob');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
// Get data URL for the image
|
|
917
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
918
|
+
// Emit event with blob and data URL
|
|
919
|
+
this.imageExported.emit({
|
|
920
|
+
blob,
|
|
921
|
+
dataUrl,
|
|
922
|
+
});
|
|
923
|
+
// Also trigger automatic download (optional - app can handle it via event)
|
|
924
|
+
const url = URL.createObjectURL(blob);
|
|
925
|
+
const link = document.createElement('a');
|
|
926
|
+
link.href = url;
|
|
927
|
+
link.download = `soccer-board-${Date.now()}.png`;
|
|
928
|
+
document.body.appendChild(link);
|
|
929
|
+
link.click();
|
|
930
|
+
document.body.removeChild(link);
|
|
931
|
+
URL.revokeObjectURL(url);
|
|
932
|
+
console.log('[SoccerBoard] PNG exported successfully');
|
|
933
|
+
}, 'image/png');
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
console.error('[SoccerBoard] Error exporting to PNG:', error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
940
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardComponent, isStandalone: true, selector: "fuzo-soccer-board", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: false, transformFunction: null }, showPlayersSidebar: { classPropertyName: "showPlayersSidebar", publicName: "showPlayersSidebar", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null }, maxPlayersPerSide: { classPropertyName: "maxPlayersPerSide", publicName: "maxPlayersPerSide", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField", imageExported: "imageExported", playerClicked: "playerClicked" }, host: { properties: { "style.width": "this.hostWidthStyle" } }, viewQueries: [{ propertyName: "fieldContainer", first: true, predicate: ["fieldContainer"], descendants: true, isSignal: true }, { propertyName: "homeHalf", first: true, predicate: ["homeHalf"], descendants: true, isSignal: true }, { propertyName: "awayHalf", first: true, predicate: ["awayHalf"], descendants: true, isSignal: true }, { propertyName: "soccerBoardContainer", first: true, predicate: ["soccerBoardContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"], dependencies: [{ kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
|
|
941
|
+
}
|
|
942
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, decorators: [{
|
|
943
|
+
type: Component,
|
|
944
|
+
args: [{ selector: 'fuzo-soccer-board', standalone: true, imports: [SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-wrapper\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n @if (showPlayersSidebar()) {\n <div class=\"players-sidebar\">\n <div\n class=\"players-grid\"\n [style.grid-template-columns]=\"playersGridColumns()\"\n >\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Soccer Board Container -->\n <div #soccerBoardContainer class=\"soccer-board-container\">\n <div\n #fieldContainer\n class=\"field-container\"\n [class.landscape]=\"isLandscape()\"\n >\n <!-- AWAY half -->\n <div #awayHalf class=\"field-half away\" [class.hidden]=\"!showAway()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of awayZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-away\">\n @for (player of awayPlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n\n <!-- HOME half -->\n <div #homeHalf class=\"field-half home\" [class.hidden]=\"!showHome()\">\n <div class=\"field-lines\">\n <div class=\"center-border\"></div>\n <div class=\"half-circle\"></div>\n <div class=\"penalty-area\"></div>\n <div class=\"goal-area\"></div>\n </div>\n\n <!-- Position zones -->\n @if (showPositions()) {\n <div class=\"position-zones\">\n @for (\n zone of homeZones();\n track zone.id + zone.coords.x + zone.coords.y\n ) {\n <div\n class=\"position-zone\"\n [style.left.%]=\"zone.coords.x\"\n [style.top.%]=\"zone.coords.y\"\n [style.width.%]=\"zone.coords.width\"\n [style.height.%]=\"zone.coords.height\"\n [style.background]=\"zone.color\"\n >\n {{ zone.label }}\n </div>\n }\n </div>\n }\n\n <!-- Players container -->\n <div class=\"players-container\" id=\"players-home\">\n @for (player of homePlayers(); track player.id) {\n @if (player.fieldX !== undefined && player.fieldY !== undefined) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [style.position]=\"'absolute'\"\n [style.left.%]=\"getPlayerPosition(player).left\"\n [style.top.%]=\"getPlayerPosition(player).top\"\n [style.transform]=\"'translate(-50%, -50%)'\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\n />\n }\n }\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%;height:100%}*{box-sizing:border-box}.soccer-board-wrapper{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .soccer-board-container{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .soccer-board-container{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .soccer-board-container{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .soccer-board-container{flex:1;min-height:0}.layout-no-sidebar .soccer-board-container{width:100%;height:100%}.players-sidebar{display:flex;flex-direction:column;background:transparent;padding:0;overflow-y:auto}.players-grid{display:grid;gap:.5rem;align-content:start}.soccer-board-container{width:100%;display:flex;justify-content:center;align-items:center;padding:var(--soccer-board-padding, 0);max-width:var(--soccer-board-max-width, none);width:var(--soccer-board-width, 100%);aspect-ratio:105/68}.field-container{display:flex;flex-direction:column;gap:0;width:100%;min-width:300px;height:auto}.field-container.landscape{flex-direction:row}.field-container.landscape .field-half{width:50%;aspect-ratio:52.5/68;background:repeating-linear-gradient(90deg,#2d5016 0px 10px,#3a6b1f 10px 20px)}.field-container.landscape .field-half.home{border-radius:0 10px 10px 0;box-shadow:10px 0 30px #0000004d}.field-container.landscape .field-half.away{border-radius:10px 0 0 10px;box-shadow:-5px 0 20px #0003}.field-container.landscape .field-half.home .center-border{inset:0 auto 0 0;width:3px;height:auto}.field-container.landscape .field-half.away .center-border{inset:0 0 0 auto;width:3px;height:auto}.field-container.landscape .field-half.home .half-circle{top:50%;left:0;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-left:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:0 999px 999px 0}.field-container.landscape .field-half.away .half-circle{inset:50% 0 auto auto;width:17.4%;height:26.9%;transform:translateY(-50%);border:2px solid rgba(255,255,255,.5);border-right:none;border-top:2px solid rgba(255,255,255,.5);border-bottom:2px solid rgba(255,255,255,.5);border-radius:999px 0 0 999px}.field-container.landscape .field-half.home .penalty-area{inset:50% 0 auto auto;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .penalty-area{inset:50% auto auto 0;transform:translateY(-50%);height:59.3%;width:31.4%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-container.landscape .field-half.home .goal-area{inset:50% 0 auto auto;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-right:none}.field-container.landscape .field-half.away .goal-area{inset:50% auto auto 0;transform:translateY(-50%);height:26.9%;width:10.5%;border:2px solid rgba(255,255,255,.5);border-left:none}.field-half{position:relative;width:100%;aspect-ratio:68/52.5;background:repeating-linear-gradient(0deg,#2d5016 0px 10px,#3a6b1f 10px 20px);overflow:hidden}.field-half.home{border-radius:0 0 10px 10px;box-shadow:0 10px 30px #0000004d}.field-half.away{border-radius:10px 10px 0 0;box-shadow:0 -5px 20px #0003}.field-half.hidden{display:none}.field-lines{position:absolute;inset:0;pointer-events:none}.field-half.home .center-border{position:absolute;background:#fffc;z-index:10;top:0;left:0;right:0;height:3px}.field-half.away .center-border{position:absolute;background:#fffc;z-index:10;bottom:0;left:0;right:0;height:3px}.field-half.home .half-circle{position:absolute;z-index:5;top:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-top:none;border-radius:0 0 999px 999px;background:transparent}.field-half.away .half-circle{position:absolute;z-index:5;bottom:0;left:50%;width:26.9%;height:17.4%;transform:translate(-50%);border:2px solid rgba(255,255,255,.5);border-bottom:none;border-radius:999px 999px 0 0;background:transparent}.field-half.home .penalty-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .penalty-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:59.3%;height:31.4%;border:2px solid rgba(255,255,255,.5);border-top:none}.field-half.home .goal-area{position:absolute;z-index:5;bottom:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-bottom:none}.field-half.away .goal-area{position:absolute;z-index:5;top:0;left:50%;transform:translate(-50%);width:26.9%;height:10.5%;border:2px solid rgba(255,255,255,.5);border-top:none}.position-zones{position:absolute;inset:0;pointer-events:none;z-index:15}.position-zones.hidden{display:none}.position-zone{position:absolute;border:2px solid rgba(255,255,255,.8);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.8);opacity:.85;transition:all .3s ease}.position-zone:hover{opacity:1;z-index:100;transform:scale(1.05)}.players-container{position:absolute;inset:0;pointer-events:none;z-index:20}.players-container>*{pointer-events:auto}\n"] }]
|
|
945
|
+
}], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], teamSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "teamSide", required: false }] }], showPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPositions", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], players: [{ type: i0.Input, args: [{ isSignal: true, alias: "players", required: false }] }], showPlayersSidebar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPlayersSidebar", required: false }] }], playersPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersPosition", required: false }] }], playersColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersColumns", required: false }] }], maxPlayersPerSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxPlayersPerSide", required: false }] }], fieldContainer: [{ type: i0.ViewChild, args: ['fieldContainer', { isSignal: true }] }], homeHalf: [{ type: i0.ViewChild, args: ['homeHalf', { isSignal: true }] }], awayHalf: [{ type: i0.ViewChild, args: ['awayHalf', { isSignal: true }] }], soccerBoardContainer: [{ type: i0.ViewChild, args: ['soccerBoardContainer', { isSignal: true }] }], playerPositioned: [{ type: i0.Output, args: ["playerPositioned"] }], playerRemovedFromField: [{ type: i0.Output, args: ["playerRemovedFromField"] }], imageExported: [{ type: i0.Output, args: ["imageExported"] }], playerClicked: [{ type: i0.Output, args: ["playerClicked"] }], hostWidthStyle: [{
|
|
946
|
+
type: HostBinding,
|
|
947
|
+
args: ['style.width']
|
|
948
|
+
}] } });
|
|
949
|
+
|
|
950
|
+
var PlayersPosition;
|
|
951
|
+
(function (PlayersPosition) {
|
|
952
|
+
PlayersPosition["Left"] = "left";
|
|
953
|
+
PlayersPosition["Right"] = "right";
|
|
954
|
+
PlayersPosition["Top"] = "top";
|
|
955
|
+
PlayersPosition["Bottom"] = "bottom";
|
|
956
|
+
})(PlayersPosition || (PlayersPosition = {}));
|
|
957
|
+
class SoccerBoardLayoutComponent {
|
|
958
|
+
// Expose enums to template
|
|
959
|
+
PlayersPosition = PlayersPosition;
|
|
960
|
+
PlayerSize = SoccerBoardPlayerSize;
|
|
961
|
+
// Inputs
|
|
962
|
+
players = input.required(...(ngDevMode ? [{ debugName: "players" }] : []));
|
|
963
|
+
orientation = input(SoccerBoardOrientation.Portrait, ...(ngDevMode ? [{ debugName: "orientation" }] : []));
|
|
964
|
+
teamSide = input(null, ...(ngDevMode ? [{ debugName: "teamSide" }] : []));
|
|
965
|
+
showPositions = input(false, ...(ngDevMode ? [{ debugName: "showPositions" }] : []));
|
|
966
|
+
fit = input(null, ...(ngDevMode ? [{ debugName: "fit" }] : []));
|
|
967
|
+
playersPosition = input(PlayersPosition.Left, ...(ngDevMode ? [{ debugName: "playersPosition" }] : []));
|
|
968
|
+
playersColumns = input(2, ...(ngDevMode ? [{ debugName: "playersColumns" }] : [])); // Number of columns for players grid
|
|
969
|
+
// ViewChild reference to soccer board
|
|
970
|
+
soccerBoard = viewChild.required('soccerBoard');
|
|
971
|
+
// Outputs
|
|
972
|
+
playerPositioned = output();
|
|
973
|
+
playerRemovedFromField = output();
|
|
974
|
+
// Computed: players available (not on field)
|
|
975
|
+
availablePlayers = computed(() => {
|
|
976
|
+
return this.players().filter((player) => player.fieldX === undefined || player.fieldY === undefined);
|
|
977
|
+
}, ...(ngDevMode ? [{ debugName: "availablePlayers" }] : []));
|
|
978
|
+
// Computed: layout classes based on players position
|
|
979
|
+
layoutClasses = computed(() => {
|
|
980
|
+
const position = this.playersPosition();
|
|
981
|
+
switch (position) {
|
|
982
|
+
case PlayersPosition.Right:
|
|
983
|
+
return 'layout-players-right';
|
|
984
|
+
case PlayersPosition.Top:
|
|
985
|
+
return 'layout-players-top';
|
|
986
|
+
case PlayersPosition.Bottom:
|
|
987
|
+
return 'layout-players-bottom';
|
|
988
|
+
default:
|
|
989
|
+
return 'layout-players-left';
|
|
990
|
+
}
|
|
991
|
+
}, ...(ngDevMode ? [{ debugName: "layoutClasses" }] : []));
|
|
992
|
+
// Computed: players grid columns
|
|
993
|
+
playersGridColumns = computed(() => {
|
|
994
|
+
return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
|
|
995
|
+
}, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
|
|
996
|
+
onPlayerDragEnd(event) {
|
|
997
|
+
const board = this.soccerBoard();
|
|
998
|
+
if (board) {
|
|
999
|
+
board.handlePlayerDrop(event);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
onPlayerPositioned(event) {
|
|
1003
|
+
this.playerPositioned.emit(event);
|
|
1004
|
+
}
|
|
1005
|
+
onPlayerRemoveFromField(event) {
|
|
1006
|
+
this.playerRemovedFromField.emit(event);
|
|
1007
|
+
}
|
|
1008
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardLayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1009
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SoccerBoardLayoutComponent, isStandalone: true, selector: "lib-soccer-board-layout", inputs: { players: { classPropertyName: "players", publicName: "players", isSignal: true, isRequired: true, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, teamSide: { classPropertyName: "teamSide", publicName: "teamSide", isSignal: true, isRequired: false, transformFunction: null }, showPositions: { classPropertyName: "showPositions", publicName: "showPositions", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, playersPosition: { classPropertyName: "playersPosition", publicName: "playersPosition", isSignal: true, isRequired: false, transformFunction: null }, playersColumns: { classPropertyName: "playersColumns", publicName: "playersColumns", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playerPositioned: "playerPositioned", playerRemovedFromField: "playerRemovedFromField" }, viewQueries: [{ propertyName: "soccerBoard", first: true, predicate: ["soccerBoard"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"soccer-board-layout\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n <div class=\"players-sidebar\">\n <div class=\"players-header\">\n <h3 class=\"players-title\">Available Players</h3>\n <span class=\"players-count\">{{ availablePlayers().length }}</span>\n </div>\n <div class=\"players-grid\" [style.grid-template-columns]=\"playersGridColumns()\">\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n />\n }\n </div>\n </div>\n\n <!-- Soccer Board Area -->\n <div class=\"board-area\">\n <fuzo-soccer-board\n #soccerBoard\n [orientation]=\"orientation()\"\n [teamSide]=\"teamSide()\"\n [showPositions]=\"showPositions()\"\n [fit]=\"fit()\"\n [players]=\"players()\"\n (playerPositioned)=\"onPlayerPositioned($event)\"\n (playerRemovedFromField)=\"onPlayerRemoveFromField($event)\"\n />\n </div>\n</div>\n", styles: [".soccer-board-layout{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .board-area{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .board-area{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .board-area{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .board-area{flex:1;min-height:0}.players-sidebar{display:flex;flex-direction:column;background:#f5f5f5;border-radius:8px;padding:1rem;overflow-y:auto;box-shadow:0 2px 8px #0000001a}.players-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:2px solid #e5e5e5}.players-title{font-size:1.125rem;font-weight:600;color:#333;margin:0}.players-count{font-size:.875rem;color:#666;background:#e5e5e5;padding:.25rem .5rem;border-radius:12px;font-weight:500}.players-grid{display:grid;gap:1rem;align-content:start}\n"], dependencies: [{ kind: "component", type: SoccerBoardComponent, selector: "fuzo-soccer-board", inputs: ["orientation", "teamSide", "showPositions", "fit", "players", "showPlayersSidebar", "playersPosition", "playersColumns", "maxPlayersPerSide"], outputs: ["playerPositioned", "playerRemovedFromField", "imageExported", "playerClicked"] }, { kind: "component", type: SoccerBoardPlayerComponent, selector: "lib-soccer-board-player", inputs: ["player", "size"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
|
|
1010
|
+
}
|
|
1011
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardLayoutComponent, decorators: [{
|
|
1012
|
+
type: Component,
|
|
1013
|
+
args: [{ selector: 'lib-soccer-board-layout', standalone: true, imports: [SoccerBoardComponent, SoccerBoardPlayerComponent], template: "<div class=\"soccer-board-layout\" [class]=\"layoutClasses()\">\n <!-- Players Sidebar -->\n <div class=\"players-sidebar\">\n <div class=\"players-header\">\n <h3 class=\"players-title\">Available Players</h3>\n <span class=\"players-count\">{{ availablePlayers().length }}</span>\n </div>\n <div class=\"players-grid\" [style.grid-template-columns]=\"playersGridColumns()\">\n @for (player of availablePlayers(); track player.id) {\n <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Medium\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n />\n }\n </div>\n </div>\n\n <!-- Soccer Board Area -->\n <div class=\"board-area\">\n <fuzo-soccer-board\n #soccerBoard\n [orientation]=\"orientation()\"\n [teamSide]=\"teamSide()\"\n [showPositions]=\"showPositions()\"\n [fit]=\"fit()\"\n [players]=\"players()\"\n (playerPositioned)=\"onPlayerPositioned($event)\"\n (playerRemovedFromField)=\"onPlayerRemoveFromField($event)\"\n />\n </div>\n</div>\n", styles: [".soccer-board-layout{display:flex;width:100%;height:100%;gap:1.5rem}.layout-players-left{flex-direction:row}.layout-players-left .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-left .board-area{flex:1;min-width:0}.layout-players-right{flex-direction:row-reverse}.layout-players-right .players-sidebar{width:300px;min-width:300px;flex-shrink:0}.layout-players-right .board-area{flex:1;min-width:0}.layout-players-top{flex-direction:column}.layout-players-top .players-sidebar{width:100%;max-height:300px}.layout-players-top .board-area{flex:1;min-height:0}.layout-players-bottom{flex-direction:column-reverse}.layout-players-bottom .players-sidebar{width:100%;max-height:300px}.layout-players-bottom .board-area{flex:1;min-height:0}.players-sidebar{display:flex;flex-direction:column;background:#f5f5f5;border-radius:8px;padding:1rem;overflow-y:auto;box-shadow:0 2px 8px #0000001a}.players-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;padding-bottom:.75rem;border-bottom:2px solid #e5e5e5}.players-title{font-size:1.125rem;font-weight:600;color:#333;margin:0}.players-count{font-size:.875rem;color:#666;background:#e5e5e5;padding:.25rem .5rem;border-radius:12px;font-weight:500}.players-grid{display:grid;gap:1rem;align-content:start}\n"] }]
|
|
1014
|
+
}], propDecorators: { players: [{ type: i0.Input, args: [{ isSignal: true, alias: "players", required: true }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], teamSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "teamSide", required: false }] }], showPositions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPositions", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], playersPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersPosition", required: false }] }], playersColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "playersColumns", required: false }] }], soccerBoard: [{ type: i0.ViewChild, args: ['soccerBoard', { isSignal: true }] }], playerPositioned: [{ type: i0.Output, args: ["playerPositioned"] }], playerRemovedFromField: [{ type: i0.Output, args: ["playerRemovedFromField"] }] } });
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Generated bundle index. Do not edit.
|
|
1018
|
+
*/
|
|
1019
|
+
|
|
1020
|
+
export { PlayersPosition, SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardLayoutComponent, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
|
|
1021
|
+
//# sourceMappingURL=fuzo-soccer-board.mjs.map
|