1/** 2 * @file Login modal on the DaisyUI base template 3 * @author Ivica Paleka 4 * @description Opens `#modalLogin` and switches its tabs. Materialize supplied 5 * both behaviours (`M.Modal` and `M.Tabs`); this replaces them with a native 6 * `<dialog>` and a few lines of delegation, so the modal needs no framework. 7 * 8 * Tabs stay anchor-driven (`<a href="#modal-tab-wallet">`) rather than becoming 9 * radio inputs: the anchors are addressable, they work as ordinary links with 10 * no JS, and the existing functional tests click them. 11 */ 12(function () { 13 "use strict"; 14 15 /** Marks the panel and its tab as the visible one. */ 16 var ACTIVE = "is-active"; 17 18 /** 19 * Show the tab panel with the given id, hiding its siblings. 20 * 21 * @param {Document|Element} root - subtree holding the tabs 22 * @param {string} panelId - id of the panel to reveal 23 * @returns {boolean} whether a matching panel was found 24 */ 25 function showTab(root, panelId) { 26 var panel = root.querySelector("#" + panelId); 27 if (!panel) return false; 28 var group = panel.parentNode; 29 Array.prototype.forEach.call(group.children, function (child) { 30 if (child.classList.contains("modal-tab-panel")) { 31 child.classList.toggle(ACTIVE, child === panel); 32 child.hidden = child !== panel; 33 } 34 }); 35 Array.prototype.forEach.call( 36 root.querySelectorAll('[href^="#modal-tab-"]'), 37 function (link) { 38 var on = link.getAttribute("href") === "#" + panelId; 39 link.classList.toggle(ACTIVE, on); 40 link.setAttribute("aria-selected", String(on)); 41 } 42 ); 43 return true; 44 } 45 46 /** Open the login dialog. Returns false when the page has no such dialog. */ 47 function openAuthModal(doc) { 48 var dialog = (doc || document).getElementById("modalLogin"); 49 if (!dialog) return false; 50 if (dialog.showModal && !dialog.open) dialog.showModal(); 51 return true; 52 } 53 54 /** Close the login dialog if it is open. */ 55 function closeAuthModal(doc) { 56 var dialog = (doc || document).getElementById("modalLogin"); 57 if (dialog && dialog.close && dialog.open) dialog.close(); 58 return !!dialog; 59 } 60 61 /** 62 * The controls the wallet card reveals once a connection has landed. 63 * 64 * `frontend/wallet` toggles these with `style.display` from its own 65 * subscription to the wallet state: `auth-button-<id>` when the wallet is 66 * connected and active (sign the 0 ALGO message), `set-active-button-<id>` 67 * when it is connected but some other wallet is active. Either one appearing 68 * means the reader has a next step, and the next step is inside the dialog. 69 */ 70 var NEXT_STEP = '[id^="auth-button-"],[id^="set-active-button-"]'; 71 72 /** 73 * Step out of the way of a wallet's own picker, then come back. 74 * 75 * `showModal()` puts the login dialog in the browser's *top layer*, which 76 * sits above every element in the normal layer no matter what z-index they 77 * carry. Wallet SDKs render their picker as ordinary DOM appended to 78 * `<body>`, so from inside this dialog the picker was painted underneath it: 79 * unreachable without closing the dialog first, and the connect attempt died 80 * along with the flow when the reader did. That is why the same wallet works 81 * on the dedicated login page, which has no dialog to be trapped behind. 82 * 83 * Neither element can be persuaded to yield -- the top layer is not 84 * negotiable from CSS -- so the dialog leaves it. It closes as the picker 85 * opens, and reopens on the wallet tab once the picker is gone, which covers 86 * a completed connection and an abandoned one alike: either way the SDK 87 * removes what it added. 88 * 89 * **Two cues, because the first one is only as good as the SDK's habits.** 90 * Watching the picker leave assumes the SDK appends its container *after* 91 * this snapshot is taken. That holds for a first connect and fails for a 92 * reconnect: a reader who arrives with a wallet session restored from a 93 * previous visit, disconnects, and connects again meets an SDK whose 94 * container is already on the body, so nothing is ever recorded as injected 95 * and the dialog never came back. They had to open it a second time to reach 96 * the Sign in button. 97 * 98 * So the connection itself is watched as well -- `NEXT_STEP` appearing inside 99 * the dialog -- which needs no assumption about anyone's DOM. It is the more 100 * reliable of the two and covers every wallet, but only when the reader 101 * completes the connection; the picker cue stays for the reader who abandons 102 * it, where there is no new control to wait for. 103 * 104 * If an SDK leaves its container behind *and* the reader abandons the 105 * connection, the dialog stays closed and the reader opens it again -- 106 * exactly the position they are in today. 107 * 108 * @param {Document} host - document holding the dialog 109 * @param {Element} dialog - the login dialog, already open 110 * @returns {boolean} whether the handoff was armed 111 */ 112 function walletHandoff(host, dialog) { 113 if (typeof MutationObserver === "undefined") return false;
114 115 /** Everything on the body before the SDK ran: the page itself. */ 116 var before = Array.prototype.slice.call(host.body.children); 117 118 /** What the SDK has appended since. Its removal is our cue to come back. */ 119 var injected = []; 120 121 var reopen = function () { 122 picker.disconnect(); 123 connection.disconnect(); 124 // Whether the reader has already reopened the dialog themselves, which 125 // they do when a cue is slow. Taking them to the wallet tab is the whole 126 // point when we reopen it; doing it to a dialog they opened and are 127 // typing into would be the second surprise in a row. 128 var closed = !dialog.open; 129 openAuthModal(host); 130 if (closed) showTab(host, "modal-tab-wallet"); 131 }; 132 133 var picker = new MutationObserver(function (records) { 134 Array.prototype.forEach.call(records, function (record) { 135 Array.prototype.forEach.call(record.addedNodes, function (node) { 136 if (node.nodeType !== 1) return; 137 if (before.indexOf(node) !== -1) return; 138 if (injected.indexOf(node) === -1) injected.push(node); 139 }); 140 }); 141 142 // Nothing has appeared yet -- the SDK may still be loading. 143 if (!injected.length) return; 144 145 var stillThere = injected.some(function (node) { 146 return host.body.contains(node); 147 }); 148 if (stillThere) return; 149 150 reopen(); 151 }); 152 153 var connection = new MutationObserver(function (records) { 154 // Only a control that *changed* counts. Reading the buttons directly 155 // would answer for a card that has never rendered, whose `style` carries 156 // no display at all, and reopen the dialog the instant it was closed. 157 var landed = Array.prototype.some.call(records, function (record) { 158 var node = record.target; 159 return ( 160 node.nodeType === 1 && 161 node.matches && 162 node.matches(NEXT_STEP) && 163 node.style.display !== "none" 164 ); 165 }); 166 if (landed) reopen(); 167 }); 168 169 picker.observe(host.body, { childList: true }); 170 connection.observe(dialog, { 171 attributes: true, 172 subtree: true, 173 attributeFilter: ["style"], 174 }); 175 closeAuthModal(host); 176 return true; 177 } 178 179 /** 180 * Delegate every interaction from one listener on the document. 181 * 182 * @param {Document} doc - document to bind to 183 * @returns {boolean} whether binding happened 184 */ 185 function wireAuthModal(doc) { 186 var host = doc || document; 187 if (!host.body || host.body.dataset.authModalBound === "1") return false; 188 host.body.dataset.authModalBound = "1"; 189 190 host.addEventListener("click", function (ev) { 191 var target = ev.target; 192 if (!target || !target.closest) return; 193 194 var opener = target.closest('[href="#modalLogin"]'); 195 if (opener) { 196 ev.preventDefault(); 197 openAuthModal(host); 198 return; 199 } 200 var closer = target.closest(".id-modal-close"); 201 if (closer) { 202 ev.preventDefault(); 203 closeAuthModal(host); 204 return; 205 } 206 var tab = target.closest('[href^="#modal-tab-"]'); 207 if (tab) { 208 ev.preventDefault(); 209 showTab(host, tab.getAttribute("href").slice(1)); 210 return; 211 } 212 // Not preventDefault'd and not returned early: the wallet package binds 213 // its own listener to this button and must still receive the click. 214 var connect = target.closest('[id^="connect-button-"]'); 215 if (connect && connect.closest("#modalLogin")) { 216 var dialog = host.getElementById("modalLogin"); 217 if (dialog && dialog.open) walletHandoff(host, dialog); 218 } 219 }); 220 return true; 221 } 222 223 /* istanbul ignore else -- in the browser we self-start; under jest we export */ 224 if (typeof module !== "undefined" && module.exports) { 225 module.exports = { 226 showTab: showTab, 227 openAuthModal: openAuthModal, 228 closeAuthModal: closeAuthModal, 229 walletHandoff: walletHandoff, 230 wireAuthModal: wireAuthModal, 231 }; 232 } else { 233 if (document.readyState === "loading") { 234 document.addEventListener("DOMContentLoaded", function () { 235 wireAuthModal(document); 236 }); 237 } else { 238 wireAuthModal(document); 239 } 240 } 241})();
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.