1/* The card menu's mail level: whether a course the reader is taking sends them its updates. 2 * 3 * The answer lives in the LMS, not here. When an enrolled card's menu opens, the learner's dashboard 4 * data is read once for the page -- api/learner_home/init, the same call the LMS's own dashboard makes 5 * -- and the level's radios are set from it: `hasOptedOutOfEmail` chooses the radio, and a course whose 6 * `isEmailEnabled` is false loses the row, because it sends no mail to choose about. Until that answer 7 * arrives the radios stay disabled; if it never does, the level says so instead of guessing. 8 * 9 * Choosing posts to api/change_email_settings the way the LMS dashboard does: `receive_emails=on` to 10 * receive, its absence to stop. Form-encoded, the marketing CSRF cookie as the token, the learner's 11 * cookies sent with it -- the same shape as the enrol and unenrol calls, so no preflight. 12 */ 13(function () { 14 'use strict'; 15 16 var dashboards = {}; // LMS address -> promise of { courseId: enrollment } 17 var live = null; 18 19 function lmsBase(lms) { return lms.replace(/\/?$/, '/'); } 20 21 function dashboard(lms) { 22 if (!dashboards[lms]) { 23 dashboards[lms] = fetch(lmsBase(lms) + 'api/learner_home/init/', { credentials: 'include' }) 24 .then(function (response) { 25 if (!response.ok) throw new Error(String(response.status)); 26 return response.json(); 27 }) 28 .then(function (data) { 29 var byCourse = {}; 30 (data.courses || []).forEach(function (course) { 31 if (course.courseRun && course.enrollment) byCourse[course.courseRun.courseId] = course.enrollment; 32 }); 33 return byCourse; 34 }); 35 // A read that failed can be asked for again the next time a menu opens. 36 dashboards[lms].catch(function () { delete dashboards[lms]; }); 37 } 38 return dashboards[lms]; 39 } 40 41 function cookie(name) { 42 var pairs = document.cookie ? document.cookie.split('; ') : []; 43 for (var i = 0; i < pairs.length; i++) { 44 var at = pairs[i].indexOf('='); 45 if (pairs[i].slice(0, at) === name) return decodeURIComponent(pairs[i].slice(at + 1)); 46 } 47 return ''; 48 } 49 50 // The same polite region card-menu.js speaks through, whichever file needs it first. 51 function say(text) { 52 if (!text) return; 53 live = live || document.querySelector('.cil-card__live'); 54 if (!live) { 55 live = document.createElement('div'); 56 live.className = 'cil-card__live'; 57 live.setAttribute('role', 'status'); 58 live.setAttribute('aria-live', 'polite'); 59 document.body.appendChild(live); 60 } 61 live.textContent = ''; 62 window.setTimeout(function () { live.textContent = text; }, 50); 63 } 64 65 function radios(group) { return group.querySelectorAll('.cil-cardMenu__radio'); } 66 67 function busy(group, isBusy) { 68 group.setAttribute('aria-busy', String(isBusy)); 69 radios(group).forEach(function (radio) { radio.disabled = isBusy; }); 70 } 71 72 function choose(group, value) { 73 group.setAttribute('data-current', value); 74 radios(group).forEach(function (radio) { radio.checked = radio.value === value; }); 75 } 76 77 /** Set the level from what the LMS said, or say it could not be read. */ 78 function apply(panel, group, enrollment) { 79 var note = group.querySelector('.cil-cardMenu__mailNote'); 80 81 if (enrollment && enrollment.isEmailEnabled === false) { 82 panel.querySelectorAll('[data-card-mail-row]').forEach(function (el) { el.hidden = true; }); 83 return; 84 } 85 if (!enrollment) { 86 if (note) { note.textContent = group.getAttribute('data-unavailable') || ''; note.hidden = false; } 87 group.setAttribute('aria-busy', 'false'); 88 return; 89 } 90 if (note) note.hidden = true; 91 choose(group, enrollment.hasOptedOutOfEmail ? 'off' : 'on'); 92 busy(group, false); 93 } 94 95 function prepare(panel) { 96 var group = panel.querySelector('[data-card-mail]'); 97 var lms = panel.getAttribute('data-card-menu-lms') || ''; 98 if (!group || !lms || group.getAttribute('data-state') === 'loading' || group.getAttribute('data-state') === 'ready') return; 99 100 group.setAttribute('data-state', 'loading'); 101 dashboard(lms).then(function (byCourse) { 102 group.setAttribute('data-state', 'ready');
103 apply(panel, group, byCourse[group.getAttribute('data-course-id')] || null); 104 }, function () { 105 group.removeAttribute('data-state'); 106 apply(panel, group, null); 107 }); 108 } 109 110 function save(radio) { 111 var group = radio.closest('[data-card-mail]'); 112 var panel = radio.closest('[data-card-menu-panel]'); 113 var lms = panel ? panel.getAttribute('data-card-menu-lms') || '' : ''; 114 var course = group.getAttribute('data-course-id') || ''; 115 var was = group.getAttribute('data-current') || ''; 116 if (!lms || !course || radio.value === was) return; 117 118 var body = new URLSearchParams(); 119 body.set('course_id', course); 120 body.set('csrfmiddlewaretoken', cookie('csrftoken-marketing')); 121 if (radio.value === 'on') body.set('receive_emails', 'on'); 122 123 busy(group, true); 124 fetch(lmsBase(lms) + 'api/change_email_settings', { 125 method: 'POST', 126 credentials: 'include', 127 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 128 body: body.toString() 129 }).then(function (response) { 130 if (!response.ok) throw new Error(String(response.status)); 131 choose(group, radio.value); 132 // Kept in step with what was just saved, so another card for this course opens right. 133 dashboard(lms).then(function (byCourse) { 134 if (byCourse[course]) byCourse[course].hasOptedOutOfEmail = radio.value === 'off'; 135 }, function () {}); 136 busy(group, false); 137 say(group.getAttribute('data-saved')); 138 }).catch(function () { 139 choose(group, was); 140 busy(group, false); 141 say(group.getAttribute('data-failed')); 142 }); 143 } 144 145 // Read as the menu opens rather than when the level does: by the time the reader reaches the row 146 // the answer is usually there, and a course that sends no mail has already lost the row. 147 document.addEventListener('click', function (event) { 148 var button = event.target.closest('[data-card-menu]'); 149 if (!button) return; 150 var card = button.closest('.cil-card'); 151 var panel = card ? card.querySelector('[data-card-menu-panel][data-card-menu-lms]') : null; 152 if (panel) prepare(panel); 153 }); 154 155 document.addEventListener('change', function (event) { 156 var radio = event.target.closest ? event.target.closest('[data-card-mail] .cil-cardMenu__radio') : null; 157 if (radio && radio.checked) save(radio); 158 }); 159})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.