@lovebowls/leagueelements
Version:
League Elements package for LoveBowls
351 lines (310 loc) • 11.7 kB
JavaScript
// Define custom event types for the LeagueMatchesAttention element
class LeagueMatchesAttentionEvent extends CustomEvent {
constructor(detail) {
super('league-matches-attention-event', {
detail,
bubbles: true, // Ensure event bubbles up through the DOM
composed: true, // Ensure event crosses shadow DOM boundaries
cancelable: true // Make the event cancelable
});
}
}
import { MOBILE_STYLES, DESKTOP_STYLES, TEMPLATE } from './LeagueMatchesAttention-styles.js';
/**
* Custom element to display matches requiring attention with paging.
*
* @element league-matches-attention
* @attr {string} data - JSON stringified League instance object
* @attr {boolean} [is-mobile] - Whether to use mobile styles
* @attr {string} [team-mapping] - JSON stringified array of {value, label} objects mapping team values to display names
*
* Emits 'league-matches-attention-event' with detail { type: 'matchClick', match }
*/
class LeagueMatchesAttention extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: 'open' });
this.league = null; // Store the league instance instead of just matches
this.currentPage = 0;
this.itemsPerPage = 5;
this.teamMapping = {};
}
static get observedAttributes() {
return ['data', 'is-mobile', 'team-mapping'];
}
connectedCallback() {
if (this.hasAttribute('team-mapping')) {
this._setTeamMapping(this.getAttribute('team-mapping'));
}
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
if (name === 'data') {
this.loadData(newValue);
} else if (name === 'is-mobile') {
this.render();
} else if (name === 'team-mapping') {
this._setTeamMapping(newValue);
this.render(); // Re-render to apply new team names
}
}
_setTeamMapping(mappingData) {
try {
if (!mappingData) {
this.teamMapping = {};
return;
}
// Parse mapping from the attribute string
const dataObj = typeof mappingData === 'string' ? JSON.parse(mappingData) : mappingData;
// Handle either array of {_id, name} objects or direct _id -> name mapping object
if (Array.isArray(dataObj)) {
// Convert array of objects to a simple map
this.teamMapping = {};
dataObj.forEach(team => {
if (team._id) {
this.teamMapping[team._id] = team.name || team._id;
}
});
} else {
// Assume we already have a direct mapping object
this.teamMapping = dataObj;
}
} catch (error) {
console.error('[LeagueMatchesAttention] Error parsing team mapping:', error);
this.teamMapping = {};
}
}
// Add a utility method to get display name for a team
getTeamDisplayName(teamId) {
if (!teamId || !this.teamMapping) {
return teamId;
}
// With standardized team model, teamMapping is a simple object map of _id -> name
const teamName = this.teamMapping[teamId];
if (teamName) {
return teamName;
}
return teamId;
}
/**
* Loads and parses the league data.
* @param {string|Object} data - League instance or JSON string
*/
async loadData(data) {
try {
if (typeof data === 'string') {
const parsedData = JSON.parse(data);
// Import League class to create a proper instance
const { League } = await import('@lovebowls/leaguejs');
this.league = new League(parsedData);
} else if (data && typeof data === 'object') {
// If it's already a League instance, use it directly
if (data.getMatchesRequiringAttention && data.getConflictingMatchIds) {
this.league = data;
} else {
// It's plain data, create a League instance
const { League } = await import('@lovebowls/leaguejs');
this.league = new League(data);
}
} else {
this.league = null;
}
this.currentPage = 0;
this.render();
this.dispatchEvent(new LeagueMatchesAttentionEvent({
type: 'dataLoaded',
league: this.league
}));
} catch (error) {
const errorMessage = 'Failed to load league data for attention matches';
this.showError(errorMessage);
console.error('Error loading league data for attention matches:', error);
this.dispatchEvent(new LeagueMatchesAttentionEvent({
type: 'error',
message: errorMessage,
error
}));
}
}
/**
* Shows an error message in the component.
* @param {string} message
*/
showError(message) {
const content = this.shadow.querySelector('.attention-matches');
if (content) {
content.innerHTML = `<div class="error">${message}</div>`;
}
}
_hasNextPage() {
if (!this.league || typeof this.league.getMatchesRequiringAttention !== 'function') {
return false;
}
const list = this.league.getMatchesRequiringAttention();
return (this.currentPage + 1) * this.itemsPerPage < list.length;
}
/**
* Renders the list of matches requiring attention for the current page.
* @returns {string}
*/
renderAttentionMatches() {
if (!this.league || typeof this.league.getMatchesRequiringAttention !== 'function') {
return '<div class="no-data">No league data available</div>';
}
const matches = this.league.getMatchesRequiringAttention();
const start = this.currentPage * this.itemsPerPage;
const pageItems = matches.slice(start, start + this.itemsPerPage);
if (pageItems.length === 0) {
if (matches.length > 0) {
return '<div class="no-data">None</div>';
}
return '<div class="no-data">None</div>';
}
const today = new Date();
today.setHours(0, 0, 0, 0);
let conflictingIds = new Set();
if (typeof this.league.getConflictingMatchIds === 'function') {
conflictingIds = this.league.getConflictingMatchIds();
}
return pageItems.map(match => {
// Get display names for teams
const homeTeamId = match.homeTeam?._id;
const awayTeamId = match.awayTeam?._id;
const homeTeamDisplay = match.homeTeam?.name || this.getTeamDisplayName(homeTeamId);
const awayTeamDisplay = match.awayTeam?.name || this.getTeamDisplayName(awayTeamId);
let warningSymbol = '';
let warningClass = '';
let tooltipText = '';
if (match.result && match.date) {
const matchDate = new Date(match.date);
matchDate.setHours(0, 0, 0, 0);
if (matchDate > today) {
tooltipText = "Result entered for a future match date.";
warningSymbol = '⚠';
warningClass = 'warning-icon-future-result';
}
} else if (conflictingIds.has(match._id)) {
tooltipText = "Scheduling conflict on this date.";
warningSymbol = '⚠';
warningClass = 'warning-icon-conflict';
} else if (match.date && !match.result) {
const matchDate = new Date(match.date);
matchDate.setHours(0, 0, 0, 0);
if (matchDate < today) {
tooltipText = "Match date passed, result pending.";
warningSymbol = '⏳';
warningClass = 'warning-icon-pending-result';
}
} else if (!match.date && !match.result) {
tooltipText = "No date set for match";
warningSymbol = '📅';
warningClass = 'warning-icon-no-date';
}
const titleAttr = tooltipText ? ` title="${this.escapeHtml(tooltipText)}"` : '';
const dataAttr = tooltipText ? ` data-attention-reason="${this.escapeHtml(tooltipText)}"` : '';
const warningSpan = warningSymbol ? `<span class="warning-icon ${warningClass}" title="${this.escapeHtml(tooltipText)}">${warningSymbol}</span>` : '';
return `
<div class="match-item list-item">
${warningSpan}
<a href="#" class="match-link list-item-text-primary" data-match-id="${match._id}"${titleAttr}${dataAttr}>
${this.escapeHtml(homeTeamDisplay)} vs ${this.escapeHtml(awayTeamDisplay)}
</a>
</div>
`;
}).join('');
}
_fillTemplate(template) {
const showPaging = this.currentPage > 0 || this._hasNextPage();
return template
.replace('{{attentionMatches}}', this.renderAttentionMatches())
.replace('{{prevDisabled}}', this.currentPage === 0 ? 'disabled' : '')
.replace('{{nextDisabled}}', this._hasNextPage() ? '' : 'disabled')
.replace('{{showPaging}}', showPaging ? '' : 'style="display: none;"');
}
render() {
const isMobile = this.getAttribute('is-mobile') === 'true';
this.shadow.innerHTML = `
<style>${isMobile ? MOBILE_STYLES : DESKTOP_STYLES}</style>
${this._fillTemplate(TEMPLATE)}
`;
this.setupEventListeners();
}
setupEventListeners() {
// Paging buttons
const prevBtn = this.shadow.querySelector('#attention-prev');
const nextBtn = this.shadow.querySelector('#attention-next');
if (prevBtn) {
prevBtn.onclick = () => {
if (this.currentPage > 0) {
this.currentPage--;
this.render();
}
};
}
if (nextBtn) {
nextBtn.onclick = () => {
if (this._hasNextPage()) {
this.currentPage++;
this.render();
}
};
}
// Match click handlers
const matchLinks = this.shadow.querySelectorAll('.match-link');
matchLinks.forEach((link, index) => {
link.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const matchId = link.dataset.matchId;
const attentionReason = link.dataset.attentionReason;
let match = null;
if (this.league && typeof this.league.getMatchesRequiringAttention === 'function') {
match = this.league.getMatchesRequiringAttention().find(m => m._id === matchId);
}
if (match) {
// Create and dispatch the event
const event = new LeagueMatchesAttentionEvent({
type: 'matchClick',
match: match,
attentionReason: attentionReason
});
const dispatchResult = this.dispatchEvent(event);
// If the event was canceled, log it
if (!dispatchResult) {
console.warn('[LeagueMatchesAttention] Event was canceled by a listener');
}
} else {
console.warn('[LeagueMatchesAttention] No match found for ID:', matchId);
}
};
});
// Add a global click handler to the shadow root to see if clicks are reaching it
this.shadow.addEventListener('click', (e) => {
if (e.target.classList.contains('match-link')) {
console.log('[LeagueMatchesAttention] Shadow root click handler - match link clicked');
}
}, { capture: true });
}
// Public API methods
setPage(pageNumber) {
if (pageNumber >= 0 && pageNumber !== this.currentPage) {
this.currentPage = pageNumber;
this.render();
}
}
// Helper method for HTML escaping
escapeHtml(unsafe = '') {
const str = String(unsafe);
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
}
import { safeDefine } from '../../utils/elementRegistry.js';
// Register the custom element
safeDefine('league-matches-attention', LeagueMatchesAttention);
export default LeagueMatchesAttention;