export class Topic { constructor() { this.overlayId = 'topic-overlay'; this.contentId = 'topic-content'; this.loaderId = 'topic-loader'; this.topicBodyId = 'topic-body'; this.topicActionsId = 'topic-actions'; this.markTopicDoneId = 'mark-topic-done'; this.markTopicPendingId = 'mark-topic-pending'; this.closeTopicId = 'close-topic'; this.contributionTextId = 'contrib-meta'; this.activeResourceType = null; this.activeResourceId = null; this.activeTopicId = null; this.handleRoadmapTopicClick = this.handleRoadmapTopicClick.bind(this); this.handleBestPracticeTopicClick = this.handleBestPracticeTopicClick.bind(this); this.handleBestPracticeTopicToggle = this.handleBestPracticeTopicToggle.bind(this); this.handleBestPracticeTopicPending = this.handleBestPracticeTopicPending.bind(this); this.close = this.close.bind(this); this.resetDOM = this.resetDOM.bind(this); this.populate = this.populate.bind(this); this.handleOverlayClick = this.handleOverlayClick.bind(this); this.markAsDone = this.markAsDone.bind(this); this.markAsPending = this.markAsPending.bind(this); this.querySvgElementsByTopicId = this.querySvgElementsByTopicId.bind(this); this.rightClickListener = this.rightClickListener.bind(this); this.isTopicDone = this.isTopicDone.bind(this); this.init = this.init.bind(this); } get loaderEl() { return document.getElementById(this.loaderId); } get markTopicDoneEl() { return document.getElementById(this.markTopicDoneId); } get markTopicPendingEl() { return document.getElementById(this.markTopicPendingId); } get topicActionsEl() { return document.getElementById(this.topicActionsId); } get contributionTextEl() { return document.getElementById(this.contributionTextId); } get contentEl() { return document.getElementById(this.contentId); } get overlayEl() { return document.getElementById(this.overlayId); } rightClickListener(e) { const groupId = e.target?.closest('g')?.dataset?.groupId; if (!groupId) { return; } e.preventDefault(); if (this.isTopicDone(groupId)) { this.markAsPending(groupId); } else { this.markAsDone(groupId); } } resetDOM(hideOverlay = false) { if (hideOverlay) { this.overlayEl.classList.add('hidden'); } else { this.overlayEl.classList.remove('hidden'); } this.loaderEl.classList.remove('hidden'); // Show loader this.topicActionsEl.classList.add('hidden'); // Hide Actions this.contributionTextEl.classList.add('hidden'); // Hide contribution text this.contentEl.replaceChildren(''); // Remove content } close() { this.resetDOM(true); this.activeResourceId = null; this.activeTopicId = null; } isTopicDone(topicId) { const normalizedGroup = topicId.replace(/^\d+-/, ''); return localStorage.getItem(normalizedGroup) === 'done'; } /** * @param {string | HTMLElement} html */ populate(html) { this.contentEl.replaceChildren(html); this.loaderEl.classList.add('hidden'); this.topicActionsEl.classList.remove('hidden'); this.contributionTextEl.classList.remove('hidden'); const isDone = this.isTopicDone(this.activeTopicId); if (isDone) { this.markTopicDoneEl.classList.add('hidden'); this.markTopicPendingEl.classList.remove('hidden'); } else { this.markTopicDoneEl.classList.remove('hidden'); this.markTopicPendingEl.classList.add('hidden'); } } renderTopicFromUrl(url) { return fetch(url) .then((res) => { return res.text(); }) .then((topicHtml) => { // It's full HTML with page body, head etc. // We only need the inner HTML of the #main-content const node = new DOMParser().parseFromString(topicHtml, 'text/html'); return node.getElementById('main-content'); }) .then((content) => { this.populate(content); }) .catch((e) => { console.error(e); this.populate('Error loading the content!'); }); } handleBestPracticeTopicToggle(e) { const { resourceId: bestPracticeId, topicId } = e.detail; if (!topicId || !bestPracticeId) { console.log('Missing topic or bestPracticeId: ', e.detail); return; } const isDone = localStorage.getItem(topicId) === 'done'; if (isDone) { this.markAsPending(topicId); } else { this.markAsDone(topicId); } } handleBestPracticeTopicPending(e) { const { resourceId: bestPracticeId, topicId } = e.detail; if (!topicId || !bestPracticeId) { console.log('Missing topic or bestPracticeId: ', e.detail); return; } this.markAsPending(topicId); } handleBestPracticeTopicClick(e) { const { resourceId: bestPracticeId, topicId } = e.detail; if (!topicId || !bestPracticeId) { console.log('Missing topic or bestPracticeId: ', e.detail); return; } this.activeResourceType = 'best-practice'; this.activeResourceId = bestPracticeId; this.activeTopicId = topicId; this.resetDOM(); this.renderTopicFromUrl(`/best-practices/${bestPracticeId}/${topicId.replaceAll(':', '/')}`); } handleRoadmapTopicClick(e) { const { resourceId: roadmapId, topicId } = e.detail; if (!topicId || !roadmapId) { console.log('Missing topic or roadmap: ', e.detail); return; } this.activeResourceType = 'roadmap'; this.activeResourceId = roadmapId; this.activeTopicId = topicId; this.resetDOM(); this.renderTopicFromUrl(`/${roadmapId}/${topicId.replaceAll(':', '/')}`); } querySvgElementsByTopicId(topicId) { const matchingElements = []; // Elements having sort order in the beginning of the group id document.querySelectorAll(`[data-group-id$="-${topicId}"]`).forEach((element) => { const foundGroupId = element?.dataset?.groupId || ''; const validGroupRegex = new RegExp(`^\\d+-${topicId}$`); if (validGroupRegex.test(foundGroupId)) { matchingElements.push(element); } }); // Elements with exact match of the topic id document.querySelectorAll(`[data-group-id="${topicId}"]`).forEach((element) => { matchingElements.push(element); }); // Matching "check:XXXX" box of the topic document.querySelectorAll(`[data-group-id="check:${topicId}"]`).forEach((element) => { matchingElements.push(element); }); return matchingElements; } markAsDone(topicId) { const updatedTopicId = topicId.replace(/^\d+-/, ''); localStorage.setItem(updatedTopicId, 'done'); this.querySvgElementsByTopicId(updatedTopicId).forEach((item) => { item?.classList?.add('done'); }); } markAsPending(topicId) { const updatedTopicId = topicId.replace(/^\d+-/, ''); localStorage.removeItem(updatedTopicId); this.querySvgElementsByTopicId(updatedTopicId).forEach((item) => { item?.classList?.remove('done'); }); } handleOverlayClick(e) { const isClickedInsideTopic = e.target.closest(`#${this.topicBodyId}`); if (!isClickedInsideTopic) { this.close(); return; } const isClickedDone = e.target.id === this.markTopicDoneId || e.target.closest(`#${this.markTopicDoneId}`); if (isClickedDone) { this.markAsDone(this.activeTopicId); this.close(); } const isClickedPending = e.target.id === this.markTopicPendingId || e.target.closest(`#${this.markTopicPendingId}`); if (isClickedPending) { this.markAsPending(this.activeTopicId); this.close(); } const isClickedClose = e.target.id === this.closeTopicId || e.target.closest(`#${this.closeTopicId}`); if (isClickedClose) { this.close(); } } init() { window.addEventListener('best-practice.topic.click', this.handleBestPracticeTopicClick); window.addEventListener('best-practice.topic.toggle', this.handleBestPracticeTopicToggle); window.addEventListener('roadmap.topic.click', this.handleRoadmapTopicClick); window.addEventListener('click', this.handleOverlayClick); window.addEventListener('contextmenu', this.rightClickListener); window.addEventListener('keydown', (e) => { if (e.key.toLowerCase() === 'escape') { this.close(); } }); } } // Initialize the topic loader const topic = new Topic(); topic.init();