@fuzo/soccer-board 0.1.0-alpha.2 → 0.2.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 +22 -2
- package/fesm2022/fuzo-soccer-board.mjs +328 -158
- package/fesm2022/fuzo-soccer-board.mjs.map +1 -1
- package/package.json +1 -1
- package/types/fuzo-soccer-board.d.ts +30 -60
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
1
2
|
import * as i0 from '@angular/core';
|
|
2
3
|
import { inject, ElementRef, input, output, signal, computed, effect, HostListener, Component, PLATFORM_ID, viewChild, HostBinding } from '@angular/core';
|
|
3
|
-
import { isPlatformBrowser } from '@angular/common';
|
|
4
4
|
import html2canvas from 'html2canvas-pro';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -201,6 +201,10 @@ class SoccerBoardPlayerComponent {
|
|
|
201
201
|
// Inputs using signals
|
|
202
202
|
player = input.required(...(ngDevMode ? [{ debugName: "player" }] : []));
|
|
203
203
|
size = input(SoccerBoardPlayerSize.Medium, ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
204
|
+
// Position inputs - when provided, the component positions itself
|
|
205
|
+
// These are percentage values (0-100) relative to parent container
|
|
206
|
+
positionLeft = input(null, ...(ngDevMode ? [{ debugName: "positionLeft" }] : []));
|
|
207
|
+
positionTop = input(null, ...(ngDevMode ? [{ debugName: "positionTop" }] : []));
|
|
204
208
|
// Outputs
|
|
205
209
|
dragStart = output();
|
|
206
210
|
dragging = output();
|
|
@@ -211,21 +215,14 @@ class SoccerBoardPlayerComponent {
|
|
|
211
215
|
isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
|
|
212
216
|
dragOffsetX = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetX" }] : []));
|
|
213
217
|
dragOffsetY = signal(0, ...(ngDevMode ? [{ debugName: "dragOffsetY" }] : []));
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
return {
|
|
224
|
-
opacity: '1',
|
|
225
|
-
cursor: 'grab',
|
|
226
|
-
zIndex: '',
|
|
227
|
-
};
|
|
228
|
-
}, ...(ngDevMode ? [{ debugName: "dragStyles" }] : []));
|
|
218
|
+
// Track if we're waiting for position update after drag
|
|
219
|
+
waitingForPositionUpdate = signal(false, ...(ngDevMode ? [{ debugName: "waitingForPositionUpdate" }] : []));
|
|
220
|
+
lastDragEndPosition = signal(null, ...(ngDevMode ? [{ debugName: "lastDragEndPosition" }] : []));
|
|
221
|
+
// Track if drag has actually started (mouse moved after mousedown)
|
|
222
|
+
isPotentialDrag = signal(false, ...(ngDevMode ? [{ debugName: "isPotentialDrag" }] : []));
|
|
223
|
+
dragStartX = signal(0, ...(ngDevMode ? [{ debugName: "dragStartX" }] : []));
|
|
224
|
+
dragStartY = signal(0, ...(ngDevMode ? [{ debugName: "dragStartY" }] : []));
|
|
225
|
+
DRAG_THRESHOLD = 5;
|
|
229
226
|
// Computed values
|
|
230
227
|
playerPhoto = computed(() => {
|
|
231
228
|
const photo = this.player()?.photo;
|
|
@@ -238,6 +235,10 @@ class SoccerBoardPlayerComponent {
|
|
|
238
235
|
const player = this.player();
|
|
239
236
|
return player?.fieldX !== undefined && player?.fieldY !== undefined;
|
|
240
237
|
}, ...(ngDevMode ? [{ debugName: "isOnField" }] : []));
|
|
238
|
+
// Determine if this component should position itself (has position inputs)
|
|
239
|
+
hasPosition = computed(() => {
|
|
240
|
+
return this.positionLeft() !== null && this.positionTop() !== null;
|
|
241
|
+
}, ...(ngDevMode ? [{ debugName: "hasPosition" }] : []));
|
|
241
242
|
// Show fieldPosition if on field, otherwise show preferredPosition
|
|
242
243
|
playerPosition = computed(() => {
|
|
243
244
|
const player = this.player();
|
|
@@ -259,62 +260,151 @@ class SoccerBoardPlayerComponent {
|
|
|
259
260
|
}
|
|
260
261
|
}, ...(ngDevMode ? [{ debugName: "sizeClasses" }] : []));
|
|
261
262
|
constructor() {
|
|
262
|
-
// Effect to apply
|
|
263
|
+
// Effect to apply position styles when not dragging
|
|
264
|
+
effect(() => {
|
|
265
|
+
const element = this.hostElement.nativeElement;
|
|
266
|
+
const dragging = this.isDragging();
|
|
267
|
+
const waiting = this.waitingForPositionUpdate();
|
|
268
|
+
const left = this.positionLeft();
|
|
269
|
+
const top = this.positionTop();
|
|
270
|
+
const hasPos = this.hasPosition();
|
|
271
|
+
const lastPos = this.lastDragEndPosition();
|
|
272
|
+
// Don't update position styles while dragging
|
|
273
|
+
if (dragging) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// If we're waiting for position update after drag ended
|
|
277
|
+
if (waiting && lastPos !== null) {
|
|
278
|
+
// Check if position has changed from what it was at drag end
|
|
279
|
+
// If position changed, we received the update and can apply the new position
|
|
280
|
+
if (left !== lastPos.left || top !== lastPos.top) {
|
|
281
|
+
this.waitingForPositionUpdate.set(false);
|
|
282
|
+
this.lastDragEndPosition.set(null);
|
|
283
|
+
// Continue to apply the new position below
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Still waiting for position update, keep the element in its current visual position
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (hasPos && left !== null && top !== null) {
|
|
291
|
+
// Player is on the field - apply absolute positioning
|
|
292
|
+
element.style.position = 'absolute';
|
|
293
|
+
element.style.left = `${left}%`;
|
|
294
|
+
element.style.top = `${top}%`;
|
|
295
|
+
element.style.transform = 'translate(-50%, -50%)';
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// Player is in sidebar - clear positioning styles
|
|
299
|
+
element.style.position = '';
|
|
300
|
+
element.style.left = '';
|
|
301
|
+
element.style.top = '';
|
|
302
|
+
element.style.transform = '';
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// Effect to apply drag visual styles (opacity, cursor, zIndex)
|
|
263
306
|
effect(() => {
|
|
264
|
-
const
|
|
307
|
+
const dragging = this.isDragging();
|
|
265
308
|
const element = this.hostElement.nativeElement;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
309
|
+
if (dragging) {
|
|
310
|
+
element.style.opacity = '0.8';
|
|
311
|
+
element.style.cursor = 'grabbing';
|
|
312
|
+
element.style.zIndex = '1000';
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
element.style.opacity = '';
|
|
316
|
+
element.style.cursor = '';
|
|
317
|
+
element.style.zIndex = '';
|
|
318
|
+
}
|
|
269
319
|
});
|
|
270
320
|
}
|
|
271
321
|
onMouseDown(event) {
|
|
322
|
+
const target = event.target;
|
|
323
|
+
if (target.closest('.remove-badge')) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
272
326
|
event.preventDefault();
|
|
273
|
-
this.
|
|
327
|
+
this.preparePotentialDrag(event.clientX, event.clientY);
|
|
274
328
|
}
|
|
275
329
|
onTouchStart(event) {
|
|
276
330
|
if (event.touches.length !== 1)
|
|
277
331
|
return;
|
|
332
|
+
const target = event.target;
|
|
333
|
+
if (target.closest('.remove-badge')) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
278
336
|
event.preventDefault();
|
|
279
337
|
const touch = event.touches[0];
|
|
280
|
-
this.
|
|
338
|
+
this.preparePotentialDrag(touch.clientX, touch.clientY);
|
|
281
339
|
}
|
|
282
|
-
|
|
283
|
-
|
|
340
|
+
preparePotentialDrag(clientX, clientY) {
|
|
341
|
+
this.dragStartX.set(clientX);
|
|
342
|
+
this.dragStartY.set(clientY);
|
|
343
|
+
this.isPotentialDrag.set(true);
|
|
284
344
|
const element = this.hostElement.nativeElement;
|
|
285
345
|
const rect = element.getBoundingClientRect();
|
|
286
346
|
this.dragOffsetX.set(clientX - rect.left);
|
|
287
347
|
this.dragOffsetY.set(clientY - rect.top);
|
|
348
|
+
}
|
|
349
|
+
startDrag(clientX, clientY) {
|
|
350
|
+
const element = this.hostElement.nativeElement;
|
|
351
|
+
const rect = element.getBoundingClientRect();
|
|
352
|
+
// IMPORTANT: Set dragging state FIRST
|
|
288
353
|
this.isDragging.set(true);
|
|
354
|
+
// Apply fixed position for smooth dragging
|
|
355
|
+
// Use exact current visual position to prevent any jump
|
|
356
|
+
element.style.position = 'fixed';
|
|
357
|
+
element.style.left = `${rect.left}px`;
|
|
358
|
+
element.style.top = `${rect.top}px`;
|
|
359
|
+
element.style.width = `${rect.width}px`;
|
|
360
|
+
element.style.height = `${rect.height}px`;
|
|
361
|
+
element.style.transform = 'none'; // Remove translate during drag
|
|
362
|
+
element.style.margin = '0';
|
|
363
|
+
element.style.pointerEvents = 'none';
|
|
289
364
|
const player = this.player();
|
|
290
|
-
console.log('[Player] Emitting dragStart', { playerId: player.id, playerName: player.name });
|
|
291
365
|
this.dragStart.emit({
|
|
292
366
|
playerId: player.id,
|
|
293
367
|
playerName: player.name,
|
|
294
368
|
});
|
|
295
369
|
}
|
|
296
370
|
onMouseMove(event) {
|
|
371
|
+
if (this.isPotentialDrag() && !this.isDragging()) {
|
|
372
|
+
const deltaX = Math.abs(event.clientX - this.dragStartX());
|
|
373
|
+
const deltaY = Math.abs(event.clientY - this.dragStartY());
|
|
374
|
+
if (deltaX > this.DRAG_THRESHOLD || deltaY > this.DRAG_THRESHOLD) {
|
|
375
|
+
this.startDrag(event.clientX, event.clientY);
|
|
376
|
+
}
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
297
379
|
if (!this.isDragging())
|
|
298
380
|
return;
|
|
299
381
|
event.preventDefault();
|
|
300
382
|
this.handleDrag(event.clientX, event.clientY);
|
|
301
383
|
}
|
|
302
384
|
onTouchMove(event) {
|
|
303
|
-
if (
|
|
385
|
+
if (event.touches.length !== 1)
|
|
304
386
|
return;
|
|
305
|
-
event.preventDefault();
|
|
306
387
|
const touch = event.touches[0];
|
|
388
|
+
if (this.isPotentialDrag() && !this.isDragging()) {
|
|
389
|
+
const deltaX = Math.abs(touch.clientX - this.dragStartX());
|
|
390
|
+
const deltaY = Math.abs(touch.clientY - this.dragStartY());
|
|
391
|
+
if (deltaX > this.DRAG_THRESHOLD || deltaY > this.DRAG_THRESHOLD) {
|
|
392
|
+
this.startDrag(touch.clientX, touch.clientY);
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (!this.isDragging())
|
|
397
|
+
return;
|
|
398
|
+
event.preventDefault();
|
|
307
399
|
this.handleDrag(touch.clientX, touch.clientY);
|
|
308
400
|
}
|
|
309
401
|
handleDrag(clientX, clientY) {
|
|
310
402
|
const element = this.hostElement.nativeElement;
|
|
311
403
|
const offsetX = this.dragOffsetX();
|
|
312
404
|
const offsetY = this.dragOffsetY();
|
|
313
|
-
//
|
|
314
|
-
element.style.position = 'fixed';
|
|
405
|
+
// Update position during drag
|
|
315
406
|
element.style.left = `${clientX - offsetX}px`;
|
|
316
407
|
element.style.top = `${clientY - offsetY}px`;
|
|
317
|
-
element.style.pointerEvents = 'none';
|
|
318
408
|
const player = this.player();
|
|
319
409
|
this.dragging.emit({
|
|
320
410
|
playerId: player.id,
|
|
@@ -325,59 +415,100 @@ class SoccerBoardPlayerComponent {
|
|
|
325
415
|
});
|
|
326
416
|
}
|
|
327
417
|
onMouseUp(event) {
|
|
418
|
+
if (this.isPotentialDrag() && !this.isDragging()) {
|
|
419
|
+
this.isPotentialDrag.set(false);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
328
422
|
if (!this.isDragging())
|
|
329
423
|
return;
|
|
330
424
|
this.endDrag(event.clientX, event.clientY);
|
|
331
425
|
}
|
|
332
426
|
onTouchEnd(event) {
|
|
427
|
+
if (this.isPotentialDrag() && !this.isDragging()) {
|
|
428
|
+
this.isPotentialDrag.set(false);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
333
431
|
if (!this.isDragging())
|
|
334
432
|
return;
|
|
335
433
|
const touch = event.changedTouches[0];
|
|
336
434
|
this.endDrag(touch.clientX, touch.clientY);
|
|
337
435
|
}
|
|
338
436
|
endDrag(clientX, clientY) {
|
|
339
|
-
console.log('[Player] End drag', { clientX, clientY });
|
|
340
|
-
this.isDragging.set(false);
|
|
341
437
|
const element = this.hostElement.nativeElement;
|
|
342
|
-
element.style.pointerEvents = '';
|
|
343
|
-
element.style.position = '';
|
|
344
|
-
element.style.left = '';
|
|
345
|
-
element.style.top = '';
|
|
346
438
|
const player = this.player();
|
|
439
|
+
const rect = element.getBoundingClientRect();
|
|
440
|
+
// Store current position inputs to detect when they change
|
|
441
|
+
const currentLeft = this.positionLeft();
|
|
442
|
+
const currentTop = this.positionTop();
|
|
443
|
+
// Emit event with all necessary information
|
|
347
444
|
const dragEndEvent = {
|
|
348
445
|
playerId: player.id,
|
|
349
446
|
clientX,
|
|
350
447
|
clientY,
|
|
351
448
|
offsetX: this.dragOffsetX(),
|
|
352
449
|
offsetY: this.dragOffsetY(),
|
|
450
|
+
elementWidth: rect.width,
|
|
451
|
+
elementHeight: rect.height,
|
|
353
452
|
};
|
|
354
|
-
console.log('[Player] Emitting dragEnd', dragEndEvent);
|
|
355
453
|
this.dragEnd.emit(dragEndEvent);
|
|
454
|
+
// Mark that we're waiting for position update
|
|
455
|
+
// Store the current position so we can detect when it changes
|
|
456
|
+
if (currentLeft !== null && currentTop !== null) {
|
|
457
|
+
this.waitingForPositionUpdate.set(true);
|
|
458
|
+
this.lastDragEndPosition.set({ left: currentLeft, top: currentTop });
|
|
459
|
+
}
|
|
460
|
+
// Clear drag state
|
|
461
|
+
this.isDragging.set(false);
|
|
462
|
+
this.isPotentialDrag.set(false);
|
|
463
|
+
// Clear drag-specific styles except position-related ones
|
|
464
|
+
// Position will be handled by the effect when new position arrives
|
|
465
|
+
element.style.width = '';
|
|
466
|
+
element.style.height = '';
|
|
467
|
+
element.style.margin = '';
|
|
468
|
+
element.style.pointerEvents = '';
|
|
469
|
+
// If we're waiting for position update, keep fixed position temporarily
|
|
470
|
+
// Otherwise, clear it and let effect handle positioning
|
|
471
|
+
if (!this.waitingForPositionUpdate()) {
|
|
472
|
+
element.style.position = '';
|
|
473
|
+
element.style.left = '';
|
|
474
|
+
element.style.top = '';
|
|
475
|
+
element.style.transform = '';
|
|
476
|
+
}
|
|
477
|
+
// Fallback: if position doesn't update within 500ms, clear waiting state
|
|
478
|
+
// This handles edge cases where the drop was outside the field
|
|
479
|
+
setTimeout(() => {
|
|
480
|
+
if (this.waitingForPositionUpdate()) {
|
|
481
|
+
this.waitingForPositionUpdate.set(false);
|
|
482
|
+
this.lastDragEndPosition.set(null);
|
|
483
|
+
// Clear fixed position and revert to input-based position
|
|
484
|
+
element.style.position = '';
|
|
485
|
+
element.style.left = '';
|
|
486
|
+
element.style.top = '';
|
|
487
|
+
element.style.transform = '';
|
|
488
|
+
}
|
|
489
|
+
}, 500);
|
|
356
490
|
}
|
|
357
491
|
onRemoveClick(event) {
|
|
358
492
|
event.stopPropagation();
|
|
359
493
|
event.preventDefault();
|
|
360
494
|
const player = this.player();
|
|
361
|
-
console.log('[Player] Remove from field clicked', player.id);
|
|
362
495
|
this.removeFromField.emit({ playerId: player.id });
|
|
363
496
|
}
|
|
364
497
|
onPlayerClick(event) {
|
|
365
|
-
|
|
366
|
-
if (this.isDragging()) {
|
|
498
|
+
if (this.isDragging() || this.isPotentialDrag()) {
|
|
367
499
|
return;
|
|
368
500
|
}
|
|
369
501
|
event.stopPropagation();
|
|
370
502
|
const player = this.player();
|
|
371
|
-
console.log('[Player] Player clicked', player.id);
|
|
372
503
|
this.playerClicked.emit({ playerId: player.id, player });
|
|
373
504
|
}
|
|
374
505
|
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
|
|
506
|
+
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 }, positionLeft: { classPropertyName: "positionLeft", publicName: "positionLeft", isSignal: true, isRequired: false, transformFunction: null }, positionTop: { classPropertyName: "positionTop", publicName: "positionTop", 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 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 (mousedown)=\"$event.stopPropagation()\"\n (touchstart)=\"$event.stopPropagation()\"\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{background-color:#ef4444!important;color:#fff!important;border-color:#fff!important;box-shadow:0 2px 8px #0006;cursor:pointer!important;top:-6px;right:-6px;pointer-events:auto!important;user-select:none;-webkit-user-select:none}.remove-badge:hover{background-color:#dc2626!important}.remove-badge:active{background-color:#b91c1c!important;transform:scale(.95)}.remove-badge:before,.remove-badge:after{pointer-events:none}.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
507
|
}
|
|
377
508
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardPlayerComponent, decorators: [{
|
|
378
509
|
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
|
|
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: [{
|
|
510
|
+
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 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 (mousedown)=\"$event.stopPropagation()\"\n (touchstart)=\"$event.stopPropagation()\"\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{background-color:#ef4444!important;color:#fff!important;border-color:#fff!important;box-shadow:0 2px 8px #0006;cursor:pointer!important;top:-6px;right:-6px;pointer-events:auto!important;user-select:none;-webkit-user-select:none}.remove-badge:hover{background-color:#dc2626!important}.remove-badge:active{background-color:#b91c1c!important;transform:scale(.95)}.remove-badge:before,.remove-badge:after{pointer-events:none}.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"] }]
|
|
511
|
+
}], ctorParameters: () => [], propDecorators: { player: [{ type: i0.Input, args: [{ isSignal: true, alias: "player", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], positionLeft: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionLeft", required: false }] }], positionTop: [{ type: i0.Input, args: [{ isSignal: true, alias: "positionTop", 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
512
|
type: HostListener,
|
|
382
513
|
args: ['mousedown', ['$event']]
|
|
383
514
|
}], onTouchStart: [{
|
|
@@ -430,6 +561,12 @@ class SoccerBoardComponent {
|
|
|
430
561
|
playerRemovedFromField = output();
|
|
431
562
|
imageExported = output();
|
|
432
563
|
playerClicked = output();
|
|
564
|
+
// Method to handle player drag start (called from template)
|
|
565
|
+
onPlayerDragStart(event) {
|
|
566
|
+
// Player component handles its own drag positioning
|
|
567
|
+
// This is just for logging/debugging
|
|
568
|
+
console.log('[SoccerBoard] Drag started', event.playerId);
|
|
569
|
+
}
|
|
433
570
|
// Method to handle player drag end (called from template)
|
|
434
571
|
onPlayerDragEnd(event) {
|
|
435
572
|
this.handlePlayerDrop(event);
|
|
@@ -503,11 +640,22 @@ class SoccerBoardComponent {
|
|
|
503
640
|
});
|
|
504
641
|
}, ...(ngDevMode ? [{ debugName: "awayZones" }] : []));
|
|
505
642
|
// Computed players by side
|
|
643
|
+
// These computed signals automatically cache results when dependencies haven't changed
|
|
644
|
+
// The @for loop uses trackBy with player.id to only re-render changed players
|
|
645
|
+
// NOTE: Even if a new array is returned, trackBy ensures only changed items re-render
|
|
506
646
|
homePlayers = computed(() => {
|
|
507
|
-
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home &&
|
|
647
|
+
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Home &&
|
|
648
|
+
player.fieldX !== undefined &&
|
|
649
|
+
player.fieldY !== undefined &&
|
|
650
|
+
!isNaN(player.fieldX) &&
|
|
651
|
+
!isNaN(player.fieldY));
|
|
508
652
|
}, ...(ngDevMode ? [{ debugName: "homePlayers" }] : []));
|
|
509
653
|
awayPlayers = computed(() => {
|
|
510
|
-
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away &&
|
|
654
|
+
return this.players().filter((player) => player.side === SoccerBoardTeamSide.Away &&
|
|
655
|
+
player.fieldX !== undefined &&
|
|
656
|
+
player.fieldY !== undefined &&
|
|
657
|
+
!isNaN(player.fieldX) &&
|
|
658
|
+
!isNaN(player.fieldY));
|
|
511
659
|
}, ...(ngDevMode ? [{ debugName: "awayPlayers" }] : []));
|
|
512
660
|
// Computed: players available (not on field)
|
|
513
661
|
availablePlayers = computed(() => {
|
|
@@ -534,12 +682,51 @@ class SoccerBoardComponent {
|
|
|
534
682
|
playersGridColumns = computed(() => {
|
|
535
683
|
return `repeat(${this.playersColumns()}, minmax(0, 1fr))`;
|
|
536
684
|
}, ...(ngDevMode ? [{ debugName: "playersGridColumns" }] : []));
|
|
685
|
+
// Computed: Map of player positions for stable rendering
|
|
686
|
+
// This ensures positions are only recalculated when players() or orientation() change
|
|
687
|
+
// Using a Map allows efficient lookups
|
|
688
|
+
playerPositions = computed(() => {
|
|
689
|
+
const positions = new Map();
|
|
690
|
+
const players = this.players();
|
|
691
|
+
// Track orientation dependency - fieldToHalfCoordinates uses it internally
|
|
692
|
+
this.orientation();
|
|
693
|
+
// Process all players with valid coordinates
|
|
694
|
+
for (const player of players) {
|
|
695
|
+
if (player.fieldX !== undefined &&
|
|
696
|
+
player.fieldY !== undefined &&
|
|
697
|
+
player.side !== undefined &&
|
|
698
|
+
!isNaN(player.fieldX) &&
|
|
699
|
+
!isNaN(player.fieldY)) {
|
|
700
|
+
// Validate coordinates are within bounds (0-100)
|
|
701
|
+
const fieldX = Math.max(0, Math.min(100, player.fieldX));
|
|
702
|
+
const fieldY = Math.max(0, Math.min(100, player.fieldY));
|
|
703
|
+
// Calculate position using validated coordinates
|
|
704
|
+
const position = this.fieldToHalfCoordinates(fieldX, fieldY, player.side);
|
|
705
|
+
positions.set(player.id, position);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return positions;
|
|
709
|
+
}, ...(ngDevMode ? [{ debugName: "playerPositions" }] : []));
|
|
537
710
|
// Helper method to get visual position for a player (public for template)
|
|
711
|
+
// Now uses the computed positions map instead of calculating on the fly
|
|
538
712
|
getPlayerPosition(player) {
|
|
539
|
-
|
|
540
|
-
|
|
713
|
+
const positions = this.playerPositions();
|
|
714
|
+
const position = positions.get(player.id);
|
|
715
|
+
if (position) {
|
|
716
|
+
return position;
|
|
541
717
|
}
|
|
542
|
-
|
|
718
|
+
// Fallback: if position is not in cache but player has coordinates, calculate it
|
|
719
|
+
// This should rarely happen, but handles edge cases
|
|
720
|
+
if (player.fieldX !== undefined &&
|
|
721
|
+
player.fieldY !== undefined &&
|
|
722
|
+
player.side !== undefined &&
|
|
723
|
+
!isNaN(player.fieldX) &&
|
|
724
|
+
!isNaN(player.fieldY)) {
|
|
725
|
+
const fieldX = Math.max(0, Math.min(100, player.fieldX));
|
|
726
|
+
const fieldY = Math.max(0, Math.min(100, player.fieldY));
|
|
727
|
+
return this.fieldToHalfCoordinates(fieldX, fieldY, player.side);
|
|
728
|
+
}
|
|
729
|
+
return { left: 0, top: 0 };
|
|
543
730
|
}
|
|
544
731
|
resizeObserver = null;
|
|
545
732
|
constructor() {
|
|
@@ -572,57 +759,106 @@ class SoccerBoardComponent {
|
|
|
572
759
|
*/
|
|
573
760
|
handlePlayerDrop(event) {
|
|
574
761
|
console.log('[SoccerBoard] handlePlayerDrop called', event);
|
|
575
|
-
|
|
762
|
+
// Calculate position immediately (don't use requestAnimationFrame as it causes timing issues)
|
|
763
|
+
// The coordinates are already correct from the drag end event
|
|
764
|
+
// clientX/clientY represent where the mouse/touch was when released
|
|
765
|
+
// offsetX/offsetY represent the offset from element's top-left to click point
|
|
766
|
+
// elementWidth/elementHeight are the dimensions of the dragged element
|
|
767
|
+
const position = this.calculateFieldPosition(event.clientX, event.clientY, event.offsetX, event.offsetY, event.elementWidth, event.elementHeight);
|
|
576
768
|
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 {
|
|
769
|
+
if (!position) {
|
|
609
770
|
console.warn('[SoccerBoard] Position is null - player not dropped on field');
|
|
771
|
+
return;
|
|
610
772
|
}
|
|
773
|
+
// Get current player state BEFORE validation to avoid race conditions
|
|
774
|
+
// Use the current players() signal value to get the latest state
|
|
775
|
+
const currentPlayers = this.players();
|
|
776
|
+
const currentPlayer = currentPlayers.find((p) => p.id === event.playerId);
|
|
777
|
+
if (!currentPlayer) {
|
|
778
|
+
console.warn('[SoccerBoard] Player not found', event.playerId);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const isMovingPlayer = currentPlayer.fieldX !== undefined && currentPlayer.fieldY !== undefined;
|
|
782
|
+
const isMovingToDifferentSide = isMovingPlayer && currentPlayer.side !== position.side;
|
|
783
|
+
// Check maximum players per side:
|
|
784
|
+
// - For new players: always check
|
|
785
|
+
// - For moving players: only check if moving to a DIFFERENT side
|
|
786
|
+
// - When moving within the same side, allow it (no limit check needed)
|
|
787
|
+
if (!isMovingPlayer || isMovingToDifferentSide) {
|
|
788
|
+
// Count players on the target side (excluding the current player if moving)
|
|
789
|
+
const playersOnTargetSide = currentPlayers.filter((p) => p.id !== event.playerId && // Exclude the player being moved
|
|
790
|
+
p.side === position.side &&
|
|
791
|
+
p.fieldX !== undefined &&
|
|
792
|
+
p.fieldY !== undefined &&
|
|
793
|
+
!isNaN(p.fieldX) &&
|
|
794
|
+
!isNaN(p.fieldY)).length;
|
|
795
|
+
if (playersOnTargetSide >= this.maxPlayersPerSide()) {
|
|
796
|
+
console.warn(`[SoccerBoard] Maximum players (${this.maxPlayersPerSide()}) reached for ${position.side} side. Cannot ${isMovingPlayer ? 'move' : 'add'} player.`);
|
|
797
|
+
return; // Don't allow drop if max players reached on target side
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Validate coordinates are within bounds
|
|
801
|
+
const fieldX = Math.max(0, Math.min(100, position.fieldX));
|
|
802
|
+
const fieldY = Math.max(0, Math.min(100, position.fieldY));
|
|
803
|
+
const tacticalPosition = this.detectTacticalPosition(fieldX, fieldY, position.side);
|
|
804
|
+
console.log('🎯 Player dropped:', {
|
|
805
|
+
playerId: event.playerId,
|
|
806
|
+
isMovingPlayer,
|
|
807
|
+
isMovingToDifferentSide,
|
|
808
|
+
position: tacticalPosition,
|
|
809
|
+
coordinates: {
|
|
810
|
+
fieldX,
|
|
811
|
+
fieldY,
|
|
812
|
+
side: position.side,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
// Emit event with position information (using validated coordinates)
|
|
816
|
+
// Emit immediately - the parent component handles the update
|
|
817
|
+
this.playerPositioned.emit({
|
|
818
|
+
playerId: event.playerId,
|
|
819
|
+
fieldX,
|
|
820
|
+
fieldY,
|
|
821
|
+
side: position.side,
|
|
822
|
+
position: tacticalPosition,
|
|
823
|
+
});
|
|
611
824
|
}
|
|
612
825
|
/**
|
|
613
826
|
* Calculates field position from client coordinates
|
|
827
|
+
* @param clientX - X coordinate of the mouse/touch event
|
|
828
|
+
* @param clientY - Y coordinate of the mouse/touch event
|
|
829
|
+
* @param offsetX - Optional: X offset from element's left edge to click point (used to calculate center)
|
|
830
|
+
* @param offsetY - Optional: Y offset from element's top edge to click point (used to calculate center)
|
|
831
|
+
* @param elementWidth - Optional: Width of the dragged element
|
|
832
|
+
* @param elementHeight - Optional: Height of the dragged element
|
|
614
833
|
*/
|
|
615
|
-
calculateFieldPosition(clientX, clientY) {
|
|
834
|
+
calculateFieldPosition(clientX, clientY, offsetX, offsetY, elementWidth, elementHeight) {
|
|
616
835
|
const homeHalfEl = this.homeHalf()?.nativeElement;
|
|
617
836
|
const awayHalfEl = this.awayHalf()?.nativeElement;
|
|
618
837
|
if (!homeHalfEl || !awayHalfEl) {
|
|
619
838
|
return null;
|
|
620
839
|
}
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
840
|
+
// Calculate the center of the dragged element
|
|
841
|
+
// clientX/clientY is where the cursor is
|
|
842
|
+
// offsetX/offsetY is the offset from the element's top-left to where the user clicked
|
|
843
|
+
// elementWidth/elementHeight are the actual dimensions of the element
|
|
844
|
+
//
|
|
845
|
+
// The element's top-left corner is at:
|
|
846
|
+
// elementLeft = clientX - offsetX
|
|
847
|
+
// elementTop = clientY - offsetY
|
|
848
|
+
//
|
|
849
|
+
// The element's center is at:
|
|
850
|
+
// centerX = elementLeft + elementWidth / 2 = clientX - offsetX + elementWidth / 2
|
|
851
|
+
// centerY = elementTop + elementHeight / 2 = clientY - offsetY + elementHeight / 2
|
|
852
|
+
let centerX = clientX;
|
|
853
|
+
let centerY = clientY;
|
|
854
|
+
if (offsetX !== undefined &&
|
|
855
|
+
offsetY !== undefined &&
|
|
856
|
+
elementWidth !== undefined &&
|
|
857
|
+
elementHeight !== undefined) {
|
|
858
|
+
// Calculate the actual center of the element
|
|
859
|
+
centerX = clientX - offsetX + elementWidth / 2;
|
|
860
|
+
centerY = clientY - offsetY + elementHeight / 2;
|
|
861
|
+
}
|
|
626
862
|
// Check HOME half
|
|
627
863
|
const homeRect = homeHalfEl.getBoundingClientRect();
|
|
628
864
|
const isInHome = centerX >= homeRect.left &&
|
|
@@ -937,85 +1173,19 @@ class SoccerBoardComponent {
|
|
|
937
1173
|
}
|
|
938
1174
|
}
|
|
939
1175
|
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"] }] });
|
|
1176
|
+
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 (dragStart)=\"onPlayerDragStart($event)\"\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 <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\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 <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\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", "positionLeft", "positionTop"], outputs: ["dragStart", "dragging", "dragEnd", "removeFromField", "playerClicked"] }] });
|
|
941
1177
|
}
|
|
942
1178
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SoccerBoardComponent, decorators: [{
|
|
943
1179
|
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"] }]
|
|
1180
|
+
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 (dragStart)=\"onPlayerDragStart($event)\"\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 <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\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 <lib-soccer-board-player\n [player]=\"player\"\n [size]=\"PlayerSize.Small\"\n [positionLeft]=\"getPlayerPosition(player).left\"\n [positionTop]=\"getPlayerPosition(player).top\"\n (dragStart)=\"onPlayerDragStart($event)\"\n (dragEnd)=\"onPlayerDragEnd($event)\"\n (removeFromField)=\"onPlayerRemoveFromField($event)\"\n (playerClicked)=\"onPlayerClick($event)\"\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
1181
|
}], 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
1182
|
type: HostBinding,
|
|
947
1183
|
args: ['style.width']
|
|
948
1184
|
}] } });
|
|
949
1185
|
|
|
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
1186
|
/**
|
|
1017
1187
|
* Generated bundle index. Do not edit.
|
|
1018
1188
|
*/
|
|
1019
1189
|
|
|
1020
|
-
export {
|
|
1190
|
+
export { SoccerBoardComponent, SoccerBoardFieldConstants, SoccerBoardFitMode, SoccerBoardOrientation, SoccerBoardPlayerComponent, SoccerBoardPlayerSize, SoccerBoardTeamSide };
|
|
1021
1191
|
//# sourceMappingURL=fuzo-soccer-board.mjs.map
|