@pimaonline/pimaonline-themepack 3.0.0 → 3.10.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. package/LICENSE.md +30 -30
  2. package/README.md +100 -69
  3. package/dist/css/main.css +1 -1
  4. package/dist/css/plugins/alt-icons.css +1 -0
  5. package/dist/css/plugins/font-awesome.css +1 -1
  6. package/dist/css/routes.css +1 -1
  7. package/dist/css/themes/aviation/styles.css +1 -1
  8. package/dist/css/themes/bct/styles.css +1 -0
  9. package/dist/css/themes/bio/styles.css +1 -0
  10. package/dist/css/themes/business/styles.css +1 -1
  11. package/dist/css/themes/fsc/styles.css +1 -1
  12. package/dist/css/themes/hrs/styles.css +1 -1
  13. package/dist/css/themes/lgm/styles.css +1 -1
  14. package/dist/css/themes/psy/styles.css +1 -0
  15. package/dist/img/theme-images/bct/Blueprint-background.png +0 -0
  16. package/dist/img/theme-images/bio/original/dna.svg +1 -0
  17. package/dist/img/theme-images/bio/original/humans.svg +1 -0
  18. package/dist/img/theme-images/bio/original/leaves.svg +33 -0
  19. package/dist/img/theme-images/bio/original/marine-bottom.svg +1 -0
  20. package/dist/img/theme-images/bio/original/marine-top.svg +1 -0
  21. package/dist/img/theme-images/bio/original/microbes.svg +1 -0
  22. package/dist/img/theme-images/ecn/arrow-2.svg +4 -4
  23. package/dist/img/theme-images/ecn/arrow.svg +4 -4
  24. package/dist/img/theme-images/ecn/point.svg +3 -3
  25. package/dist/img/theme-images/eng/button-bkg.svg +178 -178
  26. package/dist/img/theme-images/eng/halftone.svg +177 -177
  27. package/dist/img/theme-images/eng/long-button-bkg.svg +353 -353
  28. package/dist/img/theme-images/fsc/bottomwave-pinkred.svg +16 -16
  29. package/dist/img/theme-images/fsc/bottomwave-redorange.svg +16 -16
  30. package/dist/img/theme-images/fsc/bottomwave-yellow.svg +16 -16
  31. package/dist/img/theme-images/fsc/bottomwave-yelloworange.svg +16 -16
  32. package/dist/img/theme-images/fsc/mainwave-pinkred.svg +16 -16
  33. package/dist/img/theme-images/fsc/mainwave-redorange.svg +16 -16
  34. package/dist/img/theme-images/fsc/mainwave-yellow.svg +16 -16
  35. package/dist/img/theme-images/fsc/mainwave-yelloworange.svg +16 -16
  36. package/dist/img/theme-images/music/half_note.svg +5 -5
  37. package/dist/img/theme-images/psy/counseling-icon.svg +1 -0
  38. package/dist/img/theme-images/psy/psych-icon.svg +17 -0
  39. package/dist/img/theme-images/resort/flourish-left.svg +32 -32
  40. package/dist/img/theme-images/resort/flourish-main.svg +37 -37
  41. package/dist/img/theme-images/resort/flourish-right.svg +31 -31
  42. package/dist/img/theme-images/resort/separator.svg +15 -15
  43. package/dist/img/theme-images/ss/blockquote.svg +2 -2
  44. package/dist/img/theme-images/ss/list-style.svg +3 -3
  45. package/dist/img/theme-images/ss/main-large-blob.svg +3 -3
  46. package/dist/img/theme-images/ss/main-small-blob.svg +3 -3
  47. package/dist/img/theme-images/ss/small-blob.svg +3 -3
  48. package/dist/img/theme-images/ss/tall-blob.svg +3 -3
  49. package/dist/img/theme-images/widgets/separator.svg +17 -17
  50. package/dist/js/jumpTo.js +3 -3
  51. package/dist/js/scripts.js +326 -326
  52. package/dist/js/scripts2.js +1006 -541
  53. package/dist/js/themes/ecn.js +13 -0
  54. package/dist/js/themes/hrs.js +19 -0
  55. package/dist/js/themes/ss.js +197 -0
  56. package/dist/plugins/fancybox/fancybox-example.html +51 -51
  57. package/dist/plugins/fancybox/fancybox.css +72 -72
  58. package/dist/plugins/fancybox/helpers/jquery.fancybox-buttons.css +97 -97
  59. package/dist/plugins/fancybox/helpers/jquery.fancybox-buttons.js +122 -122
  60. package/dist/plugins/fancybox/helpers/jquery.fancybox-media.js +201 -201
  61. package/dist/plugins/fancybox/helpers/jquery.fancybox-thumbs.css +54 -54
  62. package/dist/plugins/fancybox/helpers/jquery.fancybox-thumbs.js +165 -165
  63. package/dist/plugins/fancybox/jquery.fancybox.css +274 -274
  64. package/dist/plugins/fancybox/jquery.fancybox.js +2018 -2018
  65. package/dist/plugins/fancybox/jquery.fancybox.pack.js +46 -46
  66. package/dist/plugins/flashcards/README.md +135 -135
  67. package/dist/plugins/flashcards/config.rb +24 -24
  68. package/dist/plugins/flashcards/css/style.css +215 -215
  69. package/dist/plugins/flashcards/flashcards-example.html +65 -65
  70. package/dist/plugins/flashcards/index.html +90 -90
  71. package/dist/plugins/flashcards/js/flash_cards.min.js +11 -11
  72. package/dist/plugins/flashcards/js/plugins/flash_cards.js +62 -62
  73. package/dist/plugins/flashcards/js/plugins/jquery.cycle.js +1147 -1147
  74. package/dist/plugins/flashcards/js/vendor/jquery-1.7.2.js +9404 -9404
  75. package/dist/plugins/flashcards/js/vendor/jquery-1.7.2.min.js +3 -3
  76. package/dist/plugins/flashcards/js/vendor/modernizr-2.5.3.min.js +3 -3
  77. package/dist/plugins/flashcards/resources/fonts/flash_cards/flash_cards.svg +20 -20
  78. package/dist/plugins/floating-particles/floating-particles.js +67 -67
  79. package/dist/plugins/font-awesome-icons/webfonts/brands.svg +1460 -0
  80. package/dist/plugins/font-awesome-icons/webfonts/fa-brands-400.ttf +0 -0
  81. package/dist/plugins/font-awesome-icons/webfonts/fa-brands-400.woff2 +0 -0
  82. package/dist/plugins/font-awesome-icons/webfonts/fa-regular-400.ttf +0 -0
  83. package/dist/plugins/font-awesome-icons/webfonts/fa-regular-400.woff2 +0 -0
  84. package/dist/plugins/font-awesome-icons/webfonts/fa-solid-900.ttf +0 -0
  85. package/dist/plugins/font-awesome-icons/webfonts/fa-solid-900.woff2 +0 -0
  86. package/dist/plugins/font-awesome-icons/webfonts/fa-v4compatibility.ttf +0 -0
  87. package/dist/plugins/font-awesome-icons/webfonts/fa-v4compatibility.woff2 +0 -0
  88. package/dist/plugins/font-awesome-icons/webfonts/regular.svg +497 -0
  89. package/dist/plugins/font-awesome-icons/webfonts/solid.svg +4178 -0
  90. package/dist/plugins/global-homepage-overrides/global-homepage-overrides.css +539 -539
  91. package/dist/plugins/global-homepage-overrides/global-homepage-overrides.html +18 -18
  92. package/dist/plugins/global-homepage-overrides/global-homepage-overrides.js +52 -52
  93. package/dist/plugins/preview-banner/preview-banner.css +125 -125
  94. package/dist/plugins/preview-banner/preview-banner.html +17 -17
  95. package/dist/plugins/preview-banner/preview-banner.js +56 -56
  96. package/package.json +42 -39
  97. package/dist/plugins/font-awesome-icons/webfonts/fa-brands-400.eot +0 -0
  98. package/dist/plugins/font-awesome-icons/webfonts/fa-brands-400.svg +0 -3570
  99. package/dist/plugins/font-awesome-icons/webfonts/fa-brands-400.woff +0 -0
  100. package/dist/plugins/font-awesome-icons/webfonts/fa-regular-400.eot +0 -0
  101. package/dist/plugins/font-awesome-icons/webfonts/fa-regular-400.svg +0 -803
  102. package/dist/plugins/font-awesome-icons/webfonts/fa-regular-400.woff +0 -0
  103. package/dist/plugins/font-awesome-icons/webfonts/fa-solid-900.eot +0 -0
  104. package/dist/plugins/font-awesome-icons/webfonts/fa-solid-900.svg +0 -4700
  105. package/dist/plugins/font-awesome-icons/webfonts/fa-solid-900.woff +0 -0
@@ -1,541 +1,1006 @@
1
- /// @description Main JS file for PimaOnline Themepack
2
- /// @dependencies jQuery 3.3.1 or later
3
-
4
- const courseBody = document.querySelector("body");
5
- const contentWrapper = document.querySelector("#content-wrapper");
6
- const secondColumn = document.querySelector("#second-column");
7
- const thirdColumn = document.querySelector("#third-column");
8
- const columnWidget = document.querySelector("#column-widget");
9
- const videoWrapper = document.querySelector("#video-wrapper");
10
- const rolePres = document.querySelectorAll('[role="presentation"]');
11
- const imageGallery = document.querySelector(".image-gallery");
12
- const vocabListWidget = document.querySelector("dl.vocab-list");
13
- const vocabTerms = document.querySelectorAll("dl.vocab-list dt");
14
- const vocabDefs = document.querySelectorAll("dl.vocab-list dd");
15
- const vocabCloseBtns = document.querySelectorAll("dl.vocab-list button");
16
- const vocabLists = document.querySelectorAll("dl[class^='vocab-list']");
17
- const mediaContainers = document.querySelectorAll(".media-container");
18
- const tabsWidgets = document.querySelectorAll(".tabs");
19
- const h5pIframes = document.querySelectorAll("iframe");
20
- const docHead = document.querySelector("head");
21
- const h5pResizerExists = docHead.querySelector("script[src='https://pima.h5p.com/js/h5p-resizer.js']");
22
- const h5pResizer = document.createElement("script");
23
- const focusReaderTooltipText = "Highlight text as you scroll";
24
-
25
- // Grid
26
- const addGrid = () => {
27
- if (secondColumn && thirdColumn) {
28
- courseBody.id = "three-column";
29
- } else if (secondColumn && !columnWidget) {
30
- courseBody.id = "two-column";
31
- } else if (columnWidget) {
32
- courseBody.id = "two-col-widget";
33
- } else if (videoWrapper) {
34
- courseBody.id = "video-grid";
35
- } else {
36
- courseBody.id = "one-column";
37
- }
38
- };
39
- addGrid();
40
-
41
- // JS to add role and aria-label to content-wrapper, second-column, and third-column
42
- const addAria = () => {
43
- contentWrapper.setAttribute("role", "main");
44
- if (secondColumn) {
45
- secondColumn.setAttribute("role", "region");
46
- secondColumn.setAttribute("aria-label", "Second column");
47
- }
48
- if (thirdColumn) {
49
- thirdColumn.setAttribute("role", "region");
50
- thirdColumn.setAttribute("aria-label", "Third column");
51
- }
52
- };
53
- addAria();
54
-
55
- // Clean up HTML
56
- const cleanMarkup = () => {
57
- // Remove role="presentation" attr from any element that has it
58
- if (rolePres) {
59
- rolePres.forEach((roleElem) => roleElem.removeAttribute("role"));
60
- }
61
- // Set functino to remove atrributes from elements
62
- const discardAttributes = (element, ...attributes) => {
63
- attributes.forEach((attribute) => element.removeAttribute(attribute));
64
- }
65
- // Remove attributes from tables
66
- const tableElems = document.querySelectorAll("table, thead, tbody, tr, th, td");
67
- tableElems.forEach((elem) => {
68
- discardAttributes(elem, "cellspacing", "cellpadding", "width", "style");
69
- });
70
- };
71
- cleanMarkup();
72
-
73
- // Helper JS for Responsive Tables
74
- const initResponsiveTables = () => {
75
- const tables = document.querySelectorAll(".display, .display-lg")
76
- for (let table = 0; table < tables.length; table++) {
77
- let headertext = [],
78
- headers = tables[table].querySelectorAll(".display table th, table.display th, .display-lg table th, table.display-lg th"),
79
- tablebody = tables[table].querySelector(".display table tbody, table.display tbody, .display-lg table tbody, table.display-lg tbody");
80
- for (let header = 0; header < headers.length; header++) {
81
- let current = headers[header];
82
- headertext.push(current.textContent.replace(/\r?\n|\r/, ""));
83
- }
84
- for (let y = 0, row; row = tablebody.rows[y]; y++) {
85
- for (let j = 0, col; col = row.cells[j]; j++) {
86
- col.setAttribute("data-th", headertext[j]);
87
- }
88
- }
89
- }
90
- }
91
- initResponsiveTables();
92
-
93
- // This is called by anchor links via onlick="" in the HTML
94
- // Added because default anchor links don't work on all browsers using D2L
95
- const jumpTo = (anchor) => {
96
- document.getElementById(anchor).scrollIntoView();
97
- }
98
-
99
- // Image gallery
100
- const callImageGallery = () => {
101
- // Create link element with font-awesome cdn and append it to the <head>
102
- const docHead = document.querySelector("head");
103
- const fontAwesomeCdn = document.createElement("link");
104
- fontAwesomeCdn.setAttribute("rel", "stylesheet");
105
- fontAwesomeCdn.setAttribute("href", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css");
106
- docHead.appendChild(fontAwesomeCdn);
107
- // Begin image gallery
108
- const imgGalleries = document.querySelectorAll(".image-gallery"),
109
- imgBoxes = document.querySelectorAll(".image-box"),
110
- modalBoxContent = `<div class="modal-box invisible">
111
- <div class="gallery-overlay"></div>
112
- <figure class="modal-box--image"><i class="fa-solid fa-x close-img"></i> <img src="#" alt="image here" /><figcaption class="img-caption"></figcaption></figure>
113
- </div>
114
- <button class="hide-gallery">Hide</button>`;
115
-
116
- // createModalBox.innerHTML = modalBoxContent;
117
- for (let imgGallery = 0; imgGallery < imgGalleries.length; imgGallery++) {
118
- imgGalleries[imgGallery].insertAdjacentHTML("afterbegin", modalBoxContent);
119
- }
120
-
121
- if (document.querySelector(".modal-box")) {
122
- const overlay = document.querySelector(".gallery-overlay"),
123
- modalBox = document.querySelector(".modal-box"),
124
- modalImg = document.querySelector(".modal-box--image img"),
125
- modalCaption = document.querySelector(".img-caption"),
126
- closeImg = document.querySelector(".close-img");
127
-
128
- for (let imgBox = 0; imgBox < imgBoxes.length; imgBox++) {
129
- imgBoxes[imgBox].onclick = function () {
130
- modalBox.classList.remove("invisible");
131
- let imgSrc = this.querySelector("img").src;
132
- modalImg.src = imgSrc;
133
- let imgCaption = this.querySelector("img").alt;
134
- modalCaption.innerHTML = imgCaption;
135
- }
136
-
137
- // Make images tab-able
138
- imgBoxes[imgBox].setAttribute("tabindex", "0");
139
- imgBoxes[imgBox].addEventListener("keydown", function (enter) {
140
- if (enter.key === "Enter") {
141
- modalBox.classList.remove("invisible");
142
- let imgSrc = this.querySelector("img").src;
143
- modalImg.src = imgSrc;
144
- let imgCaption = this.querySelector("img").alt;
145
- modalCaption.innerHTML = imgCaption;
146
- }
147
- })
148
- }
149
- overlay.onclick = () => {
150
- modalBox.classList.add("invisible");
151
- }
152
- window.onkeydown = (esc) => {
153
- if (esc.keyCode === 27) {
154
- modalBox.classList.add("invisible");
155
- }
156
- }
157
-
158
- closeImg.onclick = () => {
159
- modalBox.classList.add("invisible");
160
- }
161
- const hideGalleries = document.querySelectorAll(".hide-gallery");
162
- for (let hideGallery = 0; hideGallery < hideGalleries.length; hideGallery++) {
163
- hideGalleries[hideGallery].addEventListener("click", () => {
164
- hideGalleries[hideGallery].nextElementSibling.classList.toggle("invisible");
165
- if (hideGalleries[hideGallery].innerHTML === "Hide") {
166
- hideGalleries[hideGallery].innerHTML = "Show";
167
- } else {
168
- hideGalleries[hideGallery].innerHTML = "Hide";
169
- }
170
- });
171
- }
172
- };
173
- }
174
- if (imageGallery) {
175
- callImageGallery();
176
- }
177
-
178
- // Vocab list widget
179
- const callVocabList = () => {
180
- if (vocabCloseBtns) {
181
- for (let btn = 0; btn < vocabCloseBtns.length; btn++) {
182
- vocabCloseBtns[btn].addEventListener("click", () => {
183
- for (let node = 0; node < vocabLists[btn].children.length; node++) {
184
- if (vocabLists[btn].children[node].tagName == "DD") {
185
- vocabLists[btn].children[node].removeAttribute("style");
186
- }
187
- if (vocabLists[btn].children[node].tagName == "DT") {
188
- vocabLists[btn].children[node].removeAttribute("class");
189
- }
190
- }
191
- });
192
- }
193
- }
194
- for (let activeTerm = 0; activeTerm < vocabTerms.length; activeTerm++) {
195
- vocabTerms[activeTerm].addEventListener("click", function () {
196
- this.classList.toggle("active");
197
- let termPanel = this.nextElementSibling;
198
- if (termPanel.style.display === "block") { termPanel.removeAttribute("style"); }
199
- else { termPanel.style.display = "block"; }
200
- });
201
-
202
- vocabTerms[activeTerm].addEventListener("keydown", function (e) {
203
- if (e.key === "Enter") {
204
- this.classList.toggle("active");
205
- let termPanel = this.nextElementSibling;
206
- if (termPanel.style.display === "block") { termPanel.removeAttribute("style"); }
207
- else { termPanel.style.display = "block"; }
208
- }
209
- });
210
- }
211
- }
212
- if (vocabListWidget) { callVocabList(); }
213
-
214
- // Media Container
215
- const addMediaContainersAria = () => {
216
- mediaContainers.forEach((eachContainer, index) => {
217
- // loopID: find the current index value, convert it to its letter equivalent, then convert to lowercase
218
- let loopId = String.fromCharCode(index + 65).toLowerCase();
219
- let mediaObject = eachContainer.querySelector(".media-object");
220
- let iframe = mediaObject.firstElementChild;
221
- let mediaInfo = mediaObject.nextElementSibling;
222
-
223
- // If element DOES NOT have "aria-describedby" && it DOES have a sibling element.
224
- if (!iframe.hasAttribute("aria-describedby") && mediaInfo != null) {
225
- iframe.setAttribute("aria-describedby", `${loopId}`);
226
- mediaInfo.id = `${[loopId]}`;
227
- }
228
- });
229
- }
230
- if (mediaContainers) { addMediaContainersAria(); }
231
-
232
- //Tabs Widget
233
- const callTabsWidget = () => {
234
-
235
- let tabsWidgetsNum = 0;
236
-
237
- tabsWidgets.forEach((tab, index) => {
238
- let tabInputs = tab.querySelectorAll("input")
239
- let tabLabels = tab.querySelectorAll("label")
240
- let tabDivs = tab.querySelectorAll("div")
241
-
242
- let groupNum = index + 1;
243
-
244
- //Add region and aria-label to parent div
245
- tab.setAttribute("role", "region");
246
- tab.setAttribute("aria-label", `tab group ${groupNum}`)
247
-
248
- for (tabIndex = 0; tabIndex < tabInputs.length; tabIndex++) {
249
-
250
- let tabNum = tabsWidgetsNum + 1;
251
-
252
- //Add class, id, name, and aria-described by for inputs
253
- tabInputs[tabIndex].classList.add("tab-input");
254
- tabInputs[tabIndex].setAttribute("type", "radio")
255
- tabInputs[tabIndex].setAttribute("id", `tab${tabNum}`);
256
- tabInputs[tabIndex].setAttribute("name", `hint-group-${groupNum}`)
257
- tabInputs[tabIndex].setAttribute("aria-describedby", `tabHeading${tabNum}`)
258
- //Add class and for for labels
259
- tabLabels[tabIndex].classList.add("tab-header");
260
- tabLabels[tabIndex].setAttribute("for", `tab${tabNum}`)
261
- //Add class, tabindex, and id for divs
262
- tabDivs[tabIndex].classList.add("tab-panel")
263
- tabDivs[tabIndex].setAttribute("tabindex", 0)
264
- tabDivs[tabIndex].setAttribute("id", `tabHeading${tabNum}`)
265
- //Add attributes for hide tab
266
- if (tabIndex + 1 == tabInputs.length) {
267
- tabLabels[tabIndex].classList.add("hide-tab")
268
- tabInputs[tabIndex].checked = true;
269
- tabDivs[tabIndex].classList.add("hide-panel")
270
- }
271
- tabsWidgetsNum++;
272
- }
273
- })
274
- }
275
- if (tabsWidgets) { callTabsWidget(); }
276
-
277
- // Toggle footnotes
278
- const toggleBtns = document.querySelectorAll(".toggle-btn, .toggle-footnotes");
279
-
280
- if (document.querySelector(".toggle-btn") || document.querySelector(".toggle-footnotes")) {
281
- for (let toggleBtn = 0; toggleBtn < toggleBtns.length; toggleBtn++) {
282
- // Add tabindex
283
- toggleBtns[toggleBtn].setAttribute("tabindex", "0");
284
-
285
- // Show/hide on click
286
- toggleBtns[toggleBtn].addEventListener("click", () => {
287
- toggleBtns[toggleBtn].nextElementSibling.classList.toggle("show");
288
- })
289
-
290
- // Show/hide on enter for users who use tab
291
- toggleBtns[toggleBtn].addEventListener("keydown", (enter) => {
292
- if (enter.keyCode === 13) {
293
- toggleBtns[toggleBtn].nextElementSibling.classList.toggle("show");
294
- }
295
- })
296
- }
297
- }
298
-
299
- // Change footnotes from 'show' to 'hide'
300
- const footnotes = document.querySelector(".toggle-footnotes");
301
-
302
- if (footnotes) {
303
- footnotes.addEventListener("click", () => {
304
- footnotes.innerHTML = (footnotes.innerHTML === "[Show Footnotes]") ? "[Hide Footnotes]" : "[Show Footnotes]";
305
- })
306
- }
307
-
308
- // Animated border for HRS theme
309
- const hrsBorders = document.querySelectorAll(".hrs-border");
310
-
311
- if (hrsBorders) {
312
- for (let hrsBorder = 0; hrsBorder < hrsBorders.length; hrsBorder++) {
313
- const callAnimateBorder = new IntersectionObserver(entries => {
314
- // Loop over the entries
315
- entries.forEach(entry => {
316
- // If the element is visible
317
- if (entry.isIntersecting) {
318
- // Add the animation class
319
- entry.target.classList.add('animate-border');
320
- }
321
- });
322
- });
323
- callAnimateBorder.observe(hrsBorders[hrsBorder]);
324
- }
325
- };
326
-
327
- // Add h5p resizer dynamically
328
- h5pResizer.setAttribute("src", "https://pima.h5p.com/js/h5p-resizer.js");
329
- h5pResizer.setAttribute("charset", "UTF-8");
330
- h5pResizer.setAttribute("defer", "");
331
-
332
- h5pIframes.forEach(function (h5pIframe) {
333
- const src = h5pIframe.getAttribute("src");
334
- if (src.includes("/d2l/common/dialogs/quickLink") || src.includes("https://pima.h5p.com/content") && !h5pResizerExists) {
335
- docHead.appendChild(h5pResizer);
336
- }
337
- });
338
-
339
- if (document.querySelector("body[focus-reader]")) {
340
-
341
- // Initializations
342
- let focusOn = false;
343
-
344
- const fr_contentWrapper = document.querySelector("#content-wrapper");
345
- const fr_thisBody = document.querySelector("body[focus-reader]");
346
-
347
- // Add fontAwesome to header
348
- const fr_pageHead = document.querySelector("head");
349
- const fontAweLink = document.createElement("link");
350
- fontAweLink.rel = "stylesheet";
351
- fontAweLink.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css";
352
- fr_pageHead.append(fontAweLink);
353
-
354
- // add intersection observer to head tag
355
- const observerScript = document.createElement("script");
356
- observerScript.setAttribute("src", "https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver");
357
- fr_pageHead.append(observerScript);
358
-
359
- // Create Switches container
360
- const fr_switchesContainer = document.createElement("div");
361
- fr_switchesContainer.className = "focus-reader-switches";
362
- fr_thisBody.append(fr_switchesContainer);
363
-
364
- // Focus-text container
365
- const fr_focusTextContainer = document.createElement("div");
366
- fr_switchesContainer.append(fr_focusTextContainer);
367
-
368
- // Focus-text Icon
369
- const fr_focusTextIcon = document.createElement("p");
370
- fr_focusTextIcon.innerHTML = "Focus Text"
371
- fr_focusTextContainer.append(fr_focusTextIcon);
372
-
373
- // info icon
374
- const fr_infoIcon = document.createElement("i");
375
- fr_infoIcon.classList.add("fa-solid");
376
- fr_infoIcon.classList.add("fa-circle-info");
377
- fr_focusTextContainer.append(fr_infoIcon);
378
-
379
- // info tooltip
380
- const fr_infoTooltip = document.createElement("span");
381
- const focusReaderTooltipText = "Highlight text as you scroll";
382
- fr_infoTooltip.classList.add("info-tooltip");
383
- fr_infoTooltip.innerHTML = focusReaderTooltipText;
384
- fr_infoIcon.append(fr_infoTooltip);
385
-
386
- // Focus-text switch button
387
- const fr_focusTextSwitch = document.createElement("button");
388
- fr_focusTextSwitch.id = "focus-text";
389
- fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
390
- fr_focusTextContainer.append(fr_focusTextSwitch);
391
-
392
- // Focus-text logic
393
- fr_focusTextSwitch.addEventListener("click", () => {
394
- if (focusOn) {
395
- focusOn = !focusOn;
396
- fr_contentWrapper.removeAttribute("on");
397
- fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
398
- removeSpans(fr_contentWrapper);
399
- } else {
400
- focusOn = !focusOn;
401
- fr_contentWrapper.setAttribute("on", "");
402
- fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
403
- const elements = document.querySelectorAll("#content-wrapper .content-body :is(p, li, dd, dt, blockquote)");
404
- elements.forEach(element => {
405
- traverseAndWrapTextInSpans(element, focusText);
406
- });
407
- }
408
- });
409
-
410
- const options = {
411
- root: null,
412
- rootMargin: '-48.9% 0px',
413
- threshold: 0.00
414
- };
415
-
416
- const focusText = new IntersectionObserver(entries => {
417
- entries.forEach(entry => {
418
- if (entry.isIntersecting) {
419
- entry.target.closest("span").classList.add("focus-text");
420
- } else {
421
- entry.target.closest("span").classList.remove("focus-text");
422
- }
423
- });
424
- }, options);
425
-
426
-
427
- }
428
-
429
- function traverseAndWrapTextInSpans(node, focusText) {
430
- if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '') {
431
- // Split the text into words
432
- const words = node.textContent.split(' ');
433
-
434
- // Wrap each word in a span
435
- words.forEach(word => {
436
- if (word !== '') {
437
- const span = document.createElement("span");
438
- span.textContent = word + ' '; // Add a space after the word to preserve spacing
439
-
440
- // Observe the span
441
- focusText.observe(span);
442
-
443
- // Insert the span before the text node
444
- node.parentNode.insertBefore(span, node);
445
- }
446
- });
447
-
448
- // Remove the original text node
449
- node.parentNode.removeChild(node);
450
- } else if (node.nodeType === Node.ELEMENT_NODE) {
451
- // Recursively process child nodes
452
- Array.from(node.childNodes).forEach(childNode => {
453
- traverseAndWrapTextInSpans(childNode, focusText);
454
- });
455
- }
456
- }
457
-
458
- function removeSpans(node) {
459
- if (node.nodeType === Node.ELEMENT_NODE) {
460
- // If the node is a span with the class 'focus-text', an empty class attribute, is empty, or has no attributes, replace it with a text node
461
- if (node.tagName === 'SPAN' && (node.className === 'focus-text' || node.className === '' || node.textContent.trim() === '' || node.attributes.length === 0)) {
462
- const textNode = document.createTextNode(node.textContent);
463
- node.parentNode.insertBefore(textNode, node);
464
- node.parentNode.removeChild(node);
465
- } else {
466
- // Recursively process child nodes
467
- Array.from(node.childNodes).forEach(childNode => {
468
- removeSpans(childNode);
469
- });
470
- }
471
- }
472
- }
473
-
474
- // Reset margin-top and padding-top for content bodies that don't have headings as the first child element
475
- const ecnStylesheet = document.querySelector("[href*='themes/ecn/styles.css']");
476
- const ecnContentBodies = document.querySelectorAll(".content-body");
477
-
478
- if (ecnStylesheet) {
479
- ecnContentBodies.forEach((contentBody) => {
480
- const firstChildHeading = contentBody.querySelector(":first-child");
481
- if (firstChildHeading && firstChildHeading.tagName !== "H2" && firstChildHeading.tagName !== "H3") {
482
- contentBody.style.marginTop = "unset";
483
- contentBody.style.paddingTop = "15px";
484
- }
485
- })
486
- }
487
-
488
-
489
- // Call function with jQuery scripts
490
- const callJquery = () => {
491
- // Toggle Button's Arrow Right Points Down on Click
492
- $('.arrow-right').on('click', function () {
493
- $(this).toggleClass('arrow-down');
494
- });
495
- // TOOLTIP
496
- // Allows Screen readers to toggle a tooltip on click and to say if the tooltip is collapsed or expanded.
497
- $(".tooltip").click(function () {
498
- $(this).children(".tip-hover").toggle();
499
- if ($(this).children(".tip-hover").is(':visible')) {
500
- $(this).attr('aria-expanded', 'true');
501
- $(this).removeClass('hidden');
502
- } else {
503
- $(this).attr('aria-expanded', 'false');
504
- $(this).addClass('hidden');
505
- }
506
- });
507
- let start = 999;
508
- $('.tooltip').each(function (i) {
509
- $(this).css('z-index', start--);
510
- });
511
- $(".tooltip .video-container").parent().css("width", "450px");
512
- }
513
- callJquery();
514
-
515
- // Array for all themes that require theme specific js
516
- const customJsThemes = ["ss"];
517
-
518
- //Search links for theme styles
519
- const themeJsCheck = () => {
520
-
521
- const links = document.querySelectorAll("link");
522
-
523
- links.forEach((link) => {
524
- const href = link.getAttribute("href");
525
- customJsThemes.forEach((theme) => {
526
- // If theme requires custom js, run addThemeScript()
527
- if (href && href.includes(`/${theme}/styles.css`)) {
528
- addThemeScript(theme);
529
- }
530
- });
531
- });
532
-
533
- // Adds a script to the head for that particular theme
534
- function addThemeScript(theme) {
535
- let themeScript = document.createElement("script");
536
- themeScript.src = `../js/themes/${theme}.js`;
537
- document.head.appendChild(themeScript);
538
- }
539
- }
540
-
541
- themeJsCheck()
1
+ const columnWidget = document.querySelector("#column-widget");
2
+ const contentLockInstructions = document.querySelectorAll(".instructions");
3
+ const contentUnlockBtns = document.querySelectorAll(".unlock-btn");
4
+ const contentWrapper = document.querySelector("#content-wrapper");
5
+ const courseBody = document.querySelector("body");
6
+ const docHead = document.querySelector("head");
7
+ const flipCards = document.querySelectorAll(".flip-card-group");
8
+ const focusReaderTooltipText = "Highlight text as you scroll";
9
+ const galleryWrappers = document.querySelectorAll(".gallery-wrapper");
10
+ const h5pIframes = document.querySelectorAll("iframe");
11
+ const h5pResizer = document.createElement("script");
12
+ const h5pResizerExists = docHead.querySelector("script[src='https://pima.h5p.com/js/h5p-resizer.js']");
13
+ // This array contains CDNs for Bootstrap and Remix icon libraries stored as objects
14
+ const iconClasses = [
15
+ { class: "bi-", cdn: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" },
16
+ { class: "ri-", cdn: "https://cdn.jsdelivr.net/npm/remixicon@4.0.1/fonts/remixicon.css" }
17
+ ];
18
+ const imageGallery = document.querySelector(".image-gallery");
19
+ const imgBoxes = document.querySelectorAll(".image-box");
20
+ const imgGalleries = document.querySelectorAll(".image-gallery");
21
+ const lockedContent = document.querySelectorAll(".locked-content");
22
+ const mediaContainers = document.querySelectorAll(".media-container");
23
+ const rolePres = document.querySelectorAll('[role="presentation"]');
24
+ const secondColumn = document.querySelector("#second-column");
25
+ const tables = document.querySelectorAll(".display, .display-lg")
26
+ const tabsWidgets = document.querySelectorAll(".tabs");
27
+ const thirdColumn = document.querySelector("#third-column");
28
+ const vocabCloseBtns = document.querySelectorAll("dl.vocab-list button");
29
+ const vocabDefs = document.querySelectorAll("dl.vocab-list dd");
30
+ const vocabListWidget = document.querySelector("dl.vocab-list");
31
+ const vocabLists = document.querySelectorAll("dl[class^='vocab-list']");
32
+ const vocabTerms = document.querySelectorAll("dl.vocab-list dt");
33
+ const videoWrapper = document.querySelector("#video-wrapper");
34
+ // JS to add role and aria-label to content-wrapper, second-column, and third-column
35
+ const addAria = () => {
36
+ if (contentWrapper) {
37
+ contentWrapper.setAttribute("role", "main");
38
+ } else if (!contentWrapper) {
39
+ console.error("Document error: does not contain #content-wrapper.");
40
+ return;
41
+ }
42
+ if (secondColumn) {
43
+ secondColumn.setAttribute("role", "region");
44
+ secondColumn.setAttribute("aria-label", "Second column");
45
+ }
46
+ if (thirdColumn) {
47
+ thirdColumn.setAttribute("role", "region");
48
+ thirdColumn.setAttribute("aria-label", "Third column");
49
+ }
50
+ };
51
+ addAria();
52
+ const addGrid = () => {
53
+ if (contentWrapper && secondColumn && thirdColumn) {
54
+ courseBody.id = "three-column";
55
+ } else if (contentWrapper && secondColumn && !columnWidget) {
56
+ courseBody.id = "two-column";
57
+ } else if (contentWrapper && secondColumn && columnWidget) {
58
+ courseBody.id = "two-col-widget";
59
+ } else if ((contentWrapper && videoWrapper)) {
60
+ courseBody.id = "video-grid";
61
+ } else if (contentWrapper && !secondColumn && !thirdColumn && !columnWidget && !videoWrapper) {
62
+ courseBody.id = "one-column";
63
+ } else if (contentWrapper && !secondColumn && (thirdColumn || columnWidget)) {
64
+ console.error("Document error: <body> is missing id because #second-column doesn't exist.");
65
+ return;
66
+ } else {
67
+ console.error("Document error: unable to determine the page layout for setting <body> id.");
68
+ return;
69
+ }
70
+
71
+ const topLevelElements = document.body.children;
72
+ let foundNestedElement = false;
73
+
74
+ // Check for additional content outside #content-wrapper, #second-column, #third-column, or footer
75
+ for (let i = 0; i < topLevelElements.length; i++) {
76
+ const element = topLevelElements[i];
77
+
78
+ if (
79
+ element.id !== "content-wrapper" &&
80
+ element.id !== "second-column" &&
81
+ element.id !== "third-column" &&
82
+ element.id !== "column-widget" &&
83
+ element.tagName !== "HEADER" &&
84
+ element.tagName !== "FOOTER" &&
85
+ element.tagName !== "SCRIPT" &&
86
+ element.id !== "loom-companion-mv3" &&
87
+ element.className !== "focus-reader-switches"
88
+ ) {
89
+ foundNestedElement = true;
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (foundNestedElement) {
95
+ console.error("Document error: Additional content outside #content-wrapper, #second-column, #third-column, or footer.");
96
+ return;
97
+ }
98
+ };
99
+ addGrid();
100
+ // Media Container
101
+ const addMediaContainersAria = () => {
102
+ mediaContainers.forEach((eachContainer, index) => {
103
+ // loopID: find the current index value, convert it to its letter equivalent, then convert to lowercase
104
+ let loopId = String.fromCharCode(index + 65).toLowerCase();
105
+ let mediaObject = eachContainer.querySelector(".media-object");
106
+ let iframe = eachContainer.querySelector("iframe");
107
+ let mediaInfo = eachContainer.querySelector(".media-info");
108
+
109
+ // Check if media container items are present
110
+ if (!iframe) {
111
+ console.error("Document error: no iframe found for media container");
112
+ return;
113
+ }
114
+ if (!mediaObject) {
115
+ console.error("Document error: no media object found for media container");
116
+ }
117
+
118
+ // If element DOES NOT have "aria-describedby" && it DOES have a sibling element.
119
+ if (!iframe.hasAttribute("aria-describedby") && mediaInfo != null) {
120
+ iframe.setAttribute("aria-describedby", `${loopId}`);
121
+ mediaInfo.id = `${[loopId]}`;
122
+ }
123
+ });
124
+ }
125
+ if (mediaContainers) { addMediaContainersAria(); }
126
+ // -------- Add CDNs for Bootstrap and Remix icon libraries ---------
127
+
128
+ // The respective CDN will be added to <head> only if a page contains an icon with a prefix specific to that library. We use forEach to loop through iconClasses because that's more efficient than using multiple if statements to make sure only the necessary CDNs are added.
129
+
130
+ iconClasses.forEach(icon => {
131
+ const iconElement = document.querySelector(`[class*='${icon.class}']`);
132
+ if (iconElement) {
133
+ const metaTagRef = docHead.querySelector("meta[name='viewport']");
134
+ //Check if viewport meta tag exists
135
+ if (!metaTagRef) {
136
+ console.error("Document error: could not find viewport meta tag");
137
+ return;
138
+ }
139
+
140
+ const iconCDN = document.createElement("link");
141
+ iconCDN.setAttribute("href", icon.cdn);
142
+ iconCDN.setAttribute("rel", "stylesheet");
143
+
144
+ // Stylesheets are added after meta tag and before themepack stylesheets and scripts to ensure proper styling override
145
+ docHead.insertBefore(iconCDN, metaTagRef.nextSibling);
146
+ }
147
+ });
148
+ // Check if parent of .gallery-wrapper has .image-gallery class
149
+ const checkGalleryWrapperParent = () => {
150
+ galleryWrappers.forEach((galleryWrapper) => {
151
+ if (!galleryWrapper.parentNode.classList.contains("image-gallery")) {
152
+ console.error(`Document error: parent of .gallery-wrapper does not have the .image-gallery class.`);
153
+ return;
154
+ }
155
+ });
156
+ };
157
+ checkGalleryWrapperParent();
158
+
159
+ // Check if parent of .image-box has .gallery-wrapper class
160
+ const checkImageBoxParent = () => {
161
+ imgBoxes.forEach((imgBox) => {
162
+ if (!imgBox.parentNode.classList.contains("gallery-wrapper")) {
163
+ console.error(`Document error: parent of .image-box does not have the .gallery-wrapper class.`);
164
+ return;
165
+ }
166
+ });
167
+ };
168
+ checkImageBoxParent();
169
+
170
+ // Check if direct children of .gallery-wrapper have .image-box class
171
+ const checkGalleryWrapperChildren = () => {
172
+ galleryWrappers.forEach((galleryWrapper) => {
173
+ let directChildren = Array.from(galleryWrapper.children).every(child => child.classList.contains("image-box"));
174
+
175
+ if (!directChildren) {
176
+ console.error(`Document error: not all direct children of .gallery-wrapper have the .image-box class.`);
177
+ return;
178
+ }
179
+ });
180
+ };
181
+ checkGalleryWrapperChildren();
182
+
183
+ // Function to add Font Awesome CDN to the head
184
+ const addFontAwesomeCdn = () => {
185
+ const fontAwesomeCdn = document.createElement("link");
186
+ fontAwesomeCdn.rel = "stylesheet";
187
+ fontAwesomeCdn.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css";
188
+ docHead.appendChild(fontAwesomeCdn);
189
+ };
190
+
191
+ // Function to create modal box HTML string
192
+ const createModalBox = () => {
193
+ return `<div class="modal-box invisible">
194
+ <div class="gallery-overlay"></div>
195
+ <figure class="modal-box--image"><i class="fa-solid fa-x close-img"></i> <img src="#" alt="image here" /><figcaption class="img-caption"></figcaption></figure>
196
+ </div>
197
+ <button class="hide-gallery">Hide</button>`;
198
+ };
199
+
200
+ // Function to initialize the image gallery
201
+ const callImageGallery = () => {
202
+ // Insert modal box HTML at the beginning of each image gallery
203
+ imgGalleries.forEach((gallery) => {
204
+ gallery.insertAdjacentHTML("afterbegin", createModalBox());
205
+ });
206
+
207
+ // Select necessary elements for later use
208
+ const overlay = document.querySelector(".gallery-overlay"),
209
+ modalBox = document.querySelector(".modal-box"),
210
+ modalImg = document.querySelector(".modal-box--image img"),
211
+ modalCaption = document.querySelector(".img-caption"),
212
+ closeImg = document.querySelector(".close-img"),
213
+ hideGalleries = document.querySelectorAll(".hide-gallery");
214
+
215
+ // Function to show modal with specified image source and caption
216
+ const showModal = (imgSrc, imgCaption) => {
217
+ modalBox.classList.remove("invisible");
218
+ modalImg.src = imgSrc;
219
+ modalCaption.innerHTML = imgCaption;
220
+ };
221
+
222
+ // Function to hide the modal
223
+ const hideModal = () => {
224
+ modalBox.classList.add("invisible");
225
+ };
226
+
227
+ // Attach event listeners to each image box
228
+ imgBoxes.forEach((imgBox) => {
229
+ // Show modal on click
230
+ imgBox.addEventListener("click", function () {
231
+ showModal(this.querySelector("img").src, this.querySelector("img").alt);
232
+ });
233
+
234
+ // Make images tab-able and show modal on Enter key press
235
+ imgBox.setAttribute("tabindex", "0");
236
+ imgBox.addEventListener("keydown", function (event) {
237
+ if (event.key === "Enter") {
238
+ showModal(this.querySelector("img").src, this.querySelector("img").alt);
239
+ }
240
+ });
241
+ });
242
+
243
+ // Attach event listeners for overlay, Escape key, and close button to hide the modal
244
+ overlay.onclick = hideModal;
245
+ window.onkeydown = (event) => {
246
+ if (event.keyCode === 27) {
247
+ hideModal();
248
+ }
249
+ };
250
+ closeImg.onclick = hideModal;
251
+
252
+ // Attach event listeners for "Hide/Show" button to toggle gallery visibility
253
+ hideGalleries.forEach((hideGallery) => {
254
+ hideGallery.addEventListener("click", () => {
255
+ hideGallery.nextElementSibling.classList.toggle("invisible");
256
+ hideGallery.innerHTML = hideGallery.innerHTML === "Hide" ? "Show" : "Hide";
257
+ });
258
+ });
259
+ };
260
+
261
+ // Check if imageGallery exists before initializing the image gallery
262
+ if (imageGallery) {
263
+ // Add Font Awesome CDN and initialize the image gallery
264
+ addFontAwesomeCdn();
265
+ callImageGallery();
266
+ }
267
+ //Tabs Widget
268
+ const callTabsWidget = () => {
269
+
270
+ let tabsWidgetsNum = 0;
271
+
272
+ tabsWidgets.forEach((tab, index) => {
273
+ let tabInputs = tab.querySelectorAll("input");
274
+ let tabLabels = tab.querySelectorAll("label");
275
+ let tabDivs = tab.querySelectorAll("div");
276
+
277
+ //Check that there are more than just one tab
278
+ if (tabInputs.length < 2 || tabLabels.length < 2 || tabDivs.length < 2) {
279
+ console.error("Document error: please add more than just one tab for tabs widget");
280
+ return;
281
+ }
282
+
283
+ // Check amount of tab elements present
284
+ if (tabInputs.length < tabLabels.length || tabInputs.length < tabDivs.length) {
285
+ console.error("Document error: missing tab input(s) in tab widget");
286
+ return;
287
+ }
288
+
289
+ if (tabLabels.length < tabInputs.length || tabLabels.length < tabDivs.length) {
290
+ console.error("Document error: missing tab label(s) in tab widget");
291
+ return;
292
+ }
293
+
294
+ let groupNum = index + 1;
295
+
296
+ //Add region and aria-label to parent div
297
+ tab.setAttribute("role", "region");
298
+ tab.setAttribute("aria-label", `tab group ${groupNum}`)
299
+
300
+ for (tabIndex = 0; tabIndex < tabInputs.length; tabIndex++) {
301
+
302
+ let tabNum = tabsWidgetsNum + 1;
303
+
304
+ // Check on present variables
305
+ if (tabInputs == null) {
306
+ console.error("Document error: no inputs found for tabs widget");
307
+ return;
308
+ }
309
+
310
+ if (tabLabels == null) {
311
+ console.error("Document error: no labels found for tabs widget");
312
+ return;
313
+ }
314
+
315
+ if (tabInputs == null) {
316
+ console.error("Document error: no divs (tab panels) found for tabs widget");
317
+ return;
318
+ }
319
+
320
+ //Add class, id, name, and aria-described by for inputs
321
+ tabInputs[tabIndex].classList.add("tab-input");
322
+ tabInputs[tabIndex].setAttribute("type", "radio")
323
+ tabInputs[tabIndex].setAttribute("id", `tab${tabNum}`);
324
+ tabInputs[tabIndex].setAttribute("name", `hint-group-${groupNum}`);
325
+ tabInputs[tabIndex].setAttribute("aria-describedby", `tabHeading${tabNum}`);
326
+
327
+ //Add class and for for labels
328
+ tabLabels[tabIndex].classList.add("tab-header");
329
+ tabLabels[tabIndex].setAttribute("for", `tab${tabNum}`);
330
+
331
+ //Add class, tabindex, and id for divs
332
+ if (tabDivs[tabIndex]) {
333
+ tabDivs[tabIndex].classList.add("tab-panel");
334
+ tabDivs[tabIndex].setAttribute("tabindex", 0);
335
+ tabDivs[tabIndex].setAttribute("id", `tabHeading${tabNum}`);
336
+ }
337
+
338
+ //Add attributes for hide tab
339
+ if (tabIndex + 1 == tabInputs.length) {
340
+ tabLabels[tabIndex].classList.add("hide-tab");
341
+ tabInputs[tabIndex].checked = true;
342
+ if (tabDivs[tabIndex]) {
343
+ tabDivs[tabIndex].classList.add("hide-panel");
344
+ }
345
+ }
346
+ tabsWidgetsNum++;
347
+ }
348
+ })
349
+ }
350
+ if (tabsWidgets) { callTabsWidget(); }
351
+ // Vocab list widget
352
+ const callVocabList = () => {
353
+
354
+ const handleVocabClose = (vocabItem) => {
355
+ if (vocabItem) {
356
+ let listDefinitions = vocabItem.querySelectorAll("dd");
357
+ let listTerms = vocabItem.querySelectorAll("dt");
358
+
359
+ // If the button is clicked and it is the DD - then hide it
360
+ listDefinitions.forEach((definition) => {
361
+ definition.style.display = "none";
362
+ })
363
+
364
+ // If the button is clicked and it is the DT - then remove active class
365
+ listTerms.forEach((term) => {
366
+ term.classList.remove("active");
367
+ })
368
+ }
369
+ }
370
+
371
+ // Check if the vocab list has one or multiple items within
372
+ vocabLists.forEach((list) => {
373
+
374
+ //Count and ensure it has more than 1 term and definition
375
+ let terms = 0;
376
+ let definitions = 0;
377
+ let closeBtnPresent = false;
378
+
379
+ // If the list contains more than one set of <dt> and <dd> tags then add a close button
380
+ for (let listIndex = 0; listIndex < list.children.length; listIndex++) {
381
+ // Count terms
382
+ if (list.children[listIndex].tagName == "DT") {
383
+ list.children[listIndex].setAttribute("tabindex", "0");
384
+ terms++;
385
+ }
386
+ // Count definitions
387
+ if (list.children[listIndex].tagName == "DD") {
388
+ definitions++;
389
+ }
390
+
391
+ //Check for close all button
392
+ if (list.children[listIndex].tagName == "BUTTON") {
393
+ closeBtnPresent = true;
394
+ }
395
+ }
396
+
397
+ // Check for terms and definitions in the vocab list
398
+ if (terms < 1) {
399
+ console.error("Document error: no terms found in vocab list");
400
+ return;
401
+ }
402
+
403
+ if (definitions < 1) {
404
+ console.error("Document error: no definitions found in vocab list");
405
+ return;
406
+ }
407
+
408
+ if (terms > definitions) {
409
+ console.error("Document error: more terms than definitions in vocab list")
410
+ }
411
+
412
+ // If there are more than 2 terms and 2 definitions, then check for a button
413
+ if (terms >= 2 && definitions >= 2) {
414
+
415
+ // If there isn't a close button then add one
416
+ if (!closeBtnPresent) {
417
+ // Add a close button
418
+ const closeButton = document.createElement("button");
419
+ closeButton.textContent = "Close All"; // Set the button text as needed
420
+ // Add click event listener for button
421
+ closeButton.addEventListener("click", () => handleVocabClose(list))
422
+
423
+ // Add keydown event listener for button
424
+ closeButton.addEventListener("keydown", (event) => {
425
+ if (event.key == "Enter") {
426
+ handleVocabClose(list);
427
+ }
428
+ })
429
+ // Append the button to the end of the list
430
+ list.appendChild(closeButton);
431
+ }
432
+ // If button is present, remove it
433
+ else {
434
+ let closeBtn = list.querySelector("button");
435
+
436
+ // Add the same event listeners as if you were to add a new button
437
+ closeBtn.addEventListener("click", () => handleVocabClose(list))
438
+
439
+ closeBtn.addEventListener("keydown", (event) => {
440
+ if (event.key == "Enter") {
441
+ handleVocabClose(list);
442
+ }
443
+ })
444
+
445
+ }
446
+ } else {
447
+
448
+ // List does not have more than 2 pairs of terms and definitions
449
+
450
+ // Don't add a close button since there is only one term, but remove the button if it is present
451
+ for (let listIndex = 0; listIndex < list.children.length; listIndex++) {
452
+
453
+ //Check for close all button
454
+ if (list.children[listIndex].tagName == "BUTTON") {
455
+ closeBtnPresent = true;
456
+ }
457
+ }
458
+
459
+ if (closeBtnPresent) {
460
+ let closeBtn = list.querySelector("button");
461
+ closeBtn.style.display = "none";
462
+ }
463
+ }
464
+ })
465
+
466
+ // Loop through all the terms and apply click and keydown event
467
+ for (let activeTerm = 0; activeTerm < vocabTerms.length; activeTerm++) {
468
+ // Add click event for toggling vocab terms
469
+ vocabTerms[activeTerm].addEventListener("click", function () {
470
+ // When clicked, toggle the active class
471
+ this.classList.toggle("active");
472
+
473
+ // Target the definition <dd> element
474
+ let termPanel = this.nextElementSibling;
475
+
476
+ // Toggle the display from none to block
477
+ if (termPanel.style.display === "block") {
478
+ termPanel.style.display = "none";
479
+ } else {
480
+ termPanel.style.display = "block";
481
+ }
482
+
483
+ // Start a while loop to continue through the DOM
484
+ while (termPanel.nextElementSibling) {
485
+ // Move to the next sibling element
486
+ termPanel = termPanel.nextElementSibling;
487
+
488
+ // Check if the current element is a <dd>
489
+ if (termPanel.tagName === "DD") {
490
+ // Toggle the display from none to block
491
+ if (termPanel.style.display === "block") {
492
+ termPanel.style.display = "none";
493
+ } else {
494
+ termPanel.style.display = "block";
495
+ }
496
+ } else {
497
+ // Stop the loop if the current element is not a <dd>
498
+ break;
499
+ }
500
+ }
501
+ });
502
+
503
+
504
+ // Add keydown event for toggling vocab terms
505
+ vocabTerms[activeTerm].addEventListener("keydown", function (e) {
506
+
507
+ // When user hits enter, toggle the active class
508
+ if (e.key === "Enter") {
509
+ // When clicked, toggle the active class
510
+ this.classList.toggle("active");
511
+
512
+ // Target the definition <dd> element
513
+ let termPanel = this.nextElementSibling;
514
+
515
+ // Toggle the display from none to block
516
+ if (termPanel.style.display === "block") {
517
+ termPanel.style.display = "none";
518
+ } else {
519
+ termPanel.style.display = "block";
520
+ }
521
+ // Start a while loop to continue through the DOM
522
+ while (termPanel.nextElementSibling) {
523
+ // Move to the next sibling element
524
+ termPanel = termPanel.nextElementSibling;
525
+
526
+ // Check if the current element is a <dd>
527
+ if (termPanel.tagName === "DD") {
528
+ // Toggle the display from none to block
529
+ if (termPanel.style.display === "block") {
530
+ termPanel.style.display = "none";
531
+ } else {
532
+ termPanel.style.display = "block";
533
+ }
534
+ } else {
535
+ // Stop the loop if the current element is not a <dd>
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ });
541
+ }
542
+ }
543
+ if (vocabListWidget) { callVocabList(); }
544
+ // Clean up HTML
545
+ const cleanMarkup = () => {
546
+ // Remove role="presentation" attr from any element that has it
547
+ if (rolePres) {
548
+ rolePres.forEach((roleElem) => roleElem.removeAttribute("role"));
549
+ }
550
+ // Set functino to remove atrributes from elements
551
+ const discardAttributes = (element, ...attributes) => {
552
+ attributes.forEach((attribute) => element.removeAttribute(attribute));
553
+ }
554
+ // Remove attributes from tables
555
+ const tableElems = document.querySelectorAll("table, thead, tbody, tr, th, td");
556
+ tableElems.forEach((elem) => {
557
+ discardAttributes(elem, "cellspacing", "cellpadding", "width", "style");
558
+ });
559
+ };
560
+ cleanMarkup();
561
+ // Content Lock Widget
562
+
563
+ // Save text content of unlock button
564
+ const unlockContent = [];
565
+
566
+ contentUnlockBtns.forEach((button) => {
567
+ unlockContent.push(button.textContent)
568
+ })
569
+
570
+ // Create an object that keeps track of keys and their statuses
571
+ let contentLockData = JSON.parse(localStorage.getItem("contentLockData")) || {};
572
+
573
+ if (lockedContent) {
574
+
575
+ // Checks the URL for the course number
576
+ const currentURL = window.parent.location.href;
577
+ const match = currentURL.match(/\/content\/(\d+)/);
578
+ const courseNumber = match ? match[1] : null;
579
+
580
+ // Add event listener for storage changes
581
+ window.addEventListener("storage", (event) => {
582
+ if (courseNumber) {
583
+ handleLocalStorageUpdate(event, courseNumber);
584
+ }
585
+ });
586
+
587
+ if (courseNumber) {
588
+
589
+ // Check if the course data object has the course number
590
+ if (!contentLockData.hasOwnProperty(courseNumber)) {
591
+ contentLockData[courseNumber] = {
592
+ keys: {},
593
+ id: 0,
594
+ };
595
+ } else {
596
+ // Reset the id count to 0 when the page is loaded
597
+ contentLockData[courseNumber].id = 0;
598
+ }
599
+
600
+ // Get key number for each content area
601
+ lockedContent.forEach((contentArea, index) => {
602
+ let keyNum = contentArea.getAttribute("data-key");
603
+
604
+ // Check if key already exists within the course, if it doesn't, add the key
605
+ if (!contentLockData[courseNumber].keys[keyNum]) {
606
+
607
+ // Add the key to the course keys
608
+ contentLockData[courseNumber].keys[keyNum] = false;
609
+ }
610
+ });
611
+
612
+ // Go through each show/hide button and add click listener
613
+ contentUnlockBtns.forEach((button, index) => {
614
+
615
+ button.addEventListener("click", function () {
616
+ const keyNum = lockedContent[index].getAttribute("data-key");
617
+
618
+ // Add alert to ensure the user confirms the action to unlock the content
619
+ const confirmed = window.confirm(`Please confirm: ${unlockContent[index]}`)
620
+
621
+ if (confirmed) {
622
+
623
+
624
+ // Toggle the key status
625
+ contentLockData[courseNumber].keys[keyNum] = !contentLockData[courseNumber].keys[keyNum];
626
+
627
+ // Toggle classes based on key status
628
+ if (contentLockData[courseNumber].keys[keyNum]) {
629
+ lockedContent[index].classList.add("open");
630
+ contentLockInstructions[index].classList.add("complete");
631
+ } else {
632
+ lockedContent[index].classList.remove("open");
633
+ contentLockInstructions[index].classList.remove("complete");
634
+ }
635
+
636
+ // Save the updated contentLockData object to local storage
637
+ localStorage.setItem("contentLockData", JSON.stringify(contentLockData));
638
+
639
+ // Update the hidden content based on the key status
640
+ checkHiddenContent(courseNumber);
641
+ }
642
+ });
643
+
644
+ // Apply initial classes based on key status
645
+ const keyNum = lockedContent[index].getAttribute("data-key");
646
+
647
+ // Make sure contentLockData[courseNumber] is initialized
648
+ if (!contentLockData[courseNumber]) {
649
+ contentLockData[courseNumber] = {
650
+ keys: {},
651
+ id: 0
652
+ };
653
+ }
654
+
655
+ if (contentLockData[courseNumber].keys[keyNum]) {
656
+ lockedContent[index].classList.add("open");
657
+ contentLockInstructions[index].classList.add("complete");
658
+ }
659
+ });
660
+
661
+ // Call the checkHiddenContent function initially
662
+ checkHiddenContent(courseNumber);
663
+
664
+ // Save the updated contentLockData object to local storage
665
+ localStorage.setItem("contentLockData", JSON.stringify(contentLockData));
666
+ }
667
+ }
668
+
669
+ // Function to run when local storage is updated
670
+ function handleLocalStorageUpdate(event, courseNumber) {
671
+ if (event && event.key === "contentLockData" && event.newValue) {
672
+ // Update contentLockData variable
673
+ contentLockData = JSON.parse(event.newValue);
674
+ // Run your code here
675
+ checkHiddenContent(courseNumber);
676
+ }
677
+ }
678
+
679
+ // Check content areas function
680
+
681
+ function checkHiddenContent(courseNumber) {
682
+
683
+ // Get key number for each content area
684
+ lockedContent.forEach((contentArea, index) => {
685
+ let keyNum = contentArea.getAttribute("data-key");
686
+ // Toggle classes based on key status
687
+ if (contentLockData[courseNumber].keys[keyNum]) {
688
+ lockedContent[index].classList.add("open");
689
+ contentLockInstructions[index].classList.add("complete");
690
+ } else {
691
+ lockedContent[index].classList.remove("open");
692
+ contentLockInstructions[index].classList.remove("complete");
693
+ }
694
+ });
695
+ }
696
+ // Flip Card Widget
697
+ function callFlipCardWidget() {
698
+ // Loop through each card
699
+ flipCards.forEach((flipCardGroup) => {
700
+
701
+ let flipCard = flipCardGroup.querySelectorAll(".flip-card");
702
+ let innerFlipCard = flipCardGroup.querySelectorAll(".inner-card");
703
+ let numOfCardsInGroup = flipCardGroup.children.length;
704
+
705
+ // Check to ensure each card has the .flip-card class
706
+ if (numOfCardsInGroup !== flipCard.length) {
707
+ console.error("Document error: missing .flip-card class for flip card widget");
708
+ return; // Stop execution if there's an error
709
+ }
710
+ // Check to ensure each card has the .inner-card class
711
+ if (numOfCardsInGroup !== innerFlipCard.length) {
712
+ console.error("Document error: missing .inner-card class for flip card widget");
713
+ return; // Stop execution if there's an error
714
+ }
715
+
716
+ flipCard.forEach((card) => {
717
+ let innerFlipCard = card.querySelectorAll(".inner-card");
718
+
719
+ innerFlipCard.forEach((innerCard) => {
720
+ innerCard.setAttribute("tabindex", 0); // Add tab index to allow flip cards to be tabbable
721
+ innerCard.addEventListener("click", () => {
722
+ innerCard.offsetHeight; // Force reflow by accessing the offsetHeight property
723
+ innerCard.classList.toggle("flip");
724
+ })
725
+
726
+
727
+ // Add a keydownevent event to each card
728
+ innerCard.addEventListener("keydown", (event) => {
729
+ if (event.key === "Enter") {
730
+ // Force reflow by accessing the offsetHeight property
731
+ innerCard.offsetHeight;
732
+ innerCard.classList.toggle("flip");
733
+ }
734
+ })
735
+ })
736
+ })
737
+ })
738
+ }
739
+
740
+ // If flip cards are present in the file, run this code
741
+ if (flipCards) { callFlipCardWidget() }
742
+ if (document.querySelector("body[focus-reader]")) {
743
+
744
+ // Initializations
745
+ let focusOn = false;
746
+
747
+ const fr_contentWrapper = document.querySelector("#content-wrapper");
748
+ const fr_thisBody = document.querySelector("body[focus-reader]");
749
+
750
+ // Add fontAwesome to header
751
+ const fr_pageHead = document.querySelector("head");
752
+ const fontAweLink = document.createElement("link");
753
+ fontAweLink.rel = "stylesheet";
754
+ fontAweLink.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css";
755
+ fr_pageHead.append(fontAweLink);
756
+
757
+ // add intersection observer to head tag
758
+ const observerScript = document.createElement("script");
759
+ observerScript.setAttribute("src", "https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver");
760
+ fr_pageHead.append(observerScript);
761
+
762
+ // Create Switches container
763
+ const fr_switchesContainer = document.createElement("div");
764
+ fr_switchesContainer.className = "focus-reader-switches";
765
+ fr_thisBody.append(fr_switchesContainer);
766
+
767
+ // Focus-text container
768
+ const fr_focusTextContainer = document.createElement("div");
769
+ fr_switchesContainer.append(fr_focusTextContainer);
770
+
771
+ // Focus-text Icon
772
+ const fr_focusTextIcon = document.createElement("p");
773
+ fr_focusTextIcon.innerHTML = "Focus Text"
774
+ fr_focusTextContainer.append(fr_focusTextIcon);
775
+
776
+ // info icon
777
+ const fr_infoIcon = document.createElement("i");
778
+ fr_infoIcon.classList.add("fa-solid");
779
+ fr_infoIcon.classList.add("fa-circle-info");
780
+ fr_focusTextContainer.append(fr_infoIcon);
781
+
782
+ // info tooltip
783
+ const fr_infoTooltip = document.createElement("span");
784
+ const focusReaderTooltipText = "Highlight text as you scroll";
785
+ fr_infoTooltip.classList.add("info-tooltip");
786
+ fr_infoTooltip.innerHTML = focusReaderTooltipText;
787
+ fr_infoIcon.append(fr_infoTooltip);
788
+
789
+ // Focus-text switch button
790
+ const fr_focusTextSwitch = document.createElement("button");
791
+ fr_focusTextSwitch.id = "focus-text";
792
+ fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
793
+ fr_focusTextContainer.append(fr_focusTextSwitch);
794
+
795
+ // Focus-text logic
796
+ fr_focusTextSwitch.addEventListener("click", () => {
797
+ if (focusOn) {
798
+ focusOn = !focusOn;
799
+ fr_contentWrapper.removeAttribute("on");
800
+ fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
801
+ removeSpans(fr_contentWrapper);
802
+ } else {
803
+ focusOn = !focusOn;
804
+ fr_contentWrapper.setAttribute("on", "");
805
+ fr_focusTextSwitch.innerHTML = (focusOn) ? `<i class="fas fa-toggle-on"></i>` : `<i class="fas fa-toggle-off"></i>`;
806
+ const elements = document.querySelectorAll("#content-wrapper .content-body :is(p, li, dd, dt, blockquote)");
807
+ elements.forEach(element => {
808
+ traverseAndWrapTextInSpans(element, focusText);
809
+ });
810
+ }
811
+ });
812
+
813
+ const options = {
814
+ root: null,
815
+ rootMargin: '-48.9% 0px',
816
+ threshold: 0.00
817
+ };
818
+
819
+ const focusText = new IntersectionObserver(entries => {
820
+ entries.forEach(entry => {
821
+ if (entry.isIntersecting) {
822
+ entry.target.closest("span").classList.add("focus-text");
823
+ } else {
824
+ entry.target.closest("span").classList.remove("focus-text");
825
+ }
826
+ });
827
+ }, options);
828
+
829
+
830
+ }
831
+
832
+ function traverseAndWrapTextInSpans(node, focusText) {
833
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '') {
834
+ // Split the text into words
835
+ const words = node.textContent.split(' ');
836
+
837
+ // Wrap each word in a span
838
+ words.forEach(word => {
839
+ if (word !== '') {
840
+ const span = document.createElement("span");
841
+ span.textContent = word + ' '; // Add a space after the word to preserve spacing
842
+
843
+ // Observe the span
844
+ focusText.observe(span);
845
+
846
+ // Insert the span before the text node
847
+ node.parentNode.insertBefore(span, node);
848
+ }
849
+ });
850
+
851
+ // Remove the original text node
852
+ node.parentNode.removeChild(node);
853
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
854
+ // Recursively process child nodes
855
+ Array.from(node.childNodes).forEach(childNode => {
856
+ traverseAndWrapTextInSpans(childNode, focusText);
857
+ });
858
+ }
859
+ }
860
+
861
+ function removeSpans(node) {
862
+ if (node.nodeType === Node.ELEMENT_NODE) {
863
+ // If the node is a span with the class 'focus-text', an empty class attribute, is empty, or has no attributes, replace it with a text node
864
+ if (node.tagName === 'SPAN' && (node.className === 'focus-text' || node.className === '' || node.textContent.trim() === '' || node.attributes.length === 0)) {
865
+ const textNode = document.createTextNode(node.textContent);
866
+ node.parentNode.insertBefore(textNode, node);
867
+ node.parentNode.removeChild(node);
868
+ } else {
869
+ // Recursively process child nodes
870
+ Array.from(node.childNodes).forEach(childNode => {
871
+ removeSpans(childNode);
872
+ });
873
+ }
874
+ }
875
+ }
876
+ // Change footnotes from 'show' to 'hide'
877
+ const footnotes = document.querySelector(".toggle-footnotes");
878
+
879
+ if (footnotes) {
880
+ footnotes.addEventListener("click", () => {
881
+ footnotes.innerHTML = (footnotes.innerHTML === "[Show Footnotes]") ? "[Hide Footnotes]" : "[Show Footnotes]";
882
+ })
883
+ }
884
+ // Add h5p resizer dynamically
885
+ h5pResizer.setAttribute("src", "https://pima.h5p.com/js/h5p-resizer.js");
886
+ h5pResizer.setAttribute("charset", "UTF-8");
887
+ h5pResizer.setAttribute("defer", "");
888
+
889
+ // If any iframes are detected run this function
890
+ function checkIframes() {
891
+ h5pIframes.forEach(function (h5pIframe) {
892
+ const src = h5pIframe.getAttribute("src");
893
+ if (src.includes("/d2l/common/dialogs/quickLink") || src.includes("https://pima.h5p.com/content") || src.includes("h5p") && !h5pResizerExists) {
894
+ docHead.appendChild(h5pResizer);
895
+ }
896
+ });
897
+ }
898
+
899
+ // Call function if iframes exist
900
+ if (h5pIframes) { checkIframes() }
901
+
902
+
903
+ // Helper JS for Responsive Tables
904
+ const initResponsiveTables = () => {
905
+ for (let table = 0; table < tables.length; table++) {
906
+ let headertext = [],
907
+ headers = tables[table].querySelectorAll(".display table th, table.display th, .display-lg table th, table.display-lg th"),
908
+ tablebody = tables[table].querySelector(".display table tbody, table.display tbody, .display-lg table tbody, table.display-lg tbody");
909
+ for (let header = 0; header < headers.length; header++) {
910
+ let current = headers[header];
911
+ headertext.push(current.textContent.replace(/\r?\n|\r/, ""));
912
+ }
913
+ for (let y = 0, row; row = tablebody.rows[y]; y++) {
914
+ for (let j = 0, col; col = row.cells[j]; j++) {
915
+ col.setAttribute("data-th", headertext[j]);
916
+ }
917
+ }
918
+ }
919
+ }
920
+ if (tables) {
921
+ initResponsiveTables();
922
+ }
923
+ // Call function with jQuery scripts
924
+ const callJquery = () => {
925
+ // Toggle Button's Arrow Right Points Down on Click
926
+ $('.arrow-right').on('click', function () {
927
+ $(this).toggleClass('arrow-down');
928
+ });
929
+ // TOOLTIP
930
+ // Allows Screen readers to toggle a tooltip on click and to say if the tooltip is collapsed or expanded.
931
+ $(".tooltip").click(function () {
932
+ $(this).children(".tip-hover").toggle();
933
+ if ($(this).children(".tip-hover").is(':visible')) {
934
+ $(this).attr('aria-expanded', 'true');
935
+ $(this).removeClass('hidden');
936
+ } else {
937
+ $(this).attr('aria-expanded', 'false');
938
+ $(this).addClass('hidden');
939
+ }
940
+ });
941
+ let start = 999;
942
+ $('.tooltip').each(function (i) {
943
+ $(this).css('z-index', start--);
944
+ });
945
+ $(".tooltip .video-container").parent().css("width", "450px");
946
+ }
947
+ callJquery();
948
+ // This is called by anchor links via onlick="" in the HTML
949
+ // Added because default anchor links don't work on all browsers using D2L
950
+ const jumpTo = (anchor) => {
951
+ document.getElementById(anchor).scrollIntoView();
952
+ }
953
+ // Array for all themes that require theme specific js
954
+ const customJsThemes = ["ecn", "hrs", "ss"];
955
+
956
+ //Search links for theme styles
957
+ const themeJsCheck = () => {
958
+
959
+ const links = document.querySelectorAll("link");
960
+
961
+ links.forEach((link) => {
962
+ const href = link.getAttribute("href");
963
+ customJsThemes.forEach((theme) => {
964
+ // If theme requires custom js, run addThemeScript()
965
+ if (href && href.includes(`/${theme}/styles.css`)) {
966
+ addThemeScript(theme);
967
+ }
968
+ });
969
+ });
970
+
971
+ // Adds a script to the head for that particular theme
972
+ function addThemeScript(theme) {
973
+ let themeScript = document.createElement("script");
974
+ // URL references theme-specific js module from CDN
975
+ themeScript.src = `https://cdn.jsdelivr.net/npm/@pimaonline/pimaonline-themepack/dist/js/themes/${theme}.js`;
976
+ document.head.appendChild(themeScript);
977
+ }
978
+ }
979
+
980
+ themeJsCheck()
981
+ // Toggle footnotes
982
+ const toggleBtns = document.querySelectorAll(".toggle-btn, .toggle-footnotes");
983
+
984
+ if (toggleBtns) {
985
+ if (document.querySelector(".toggle-btn") || document.querySelector(".toggle-footnotes")) {
986
+ for (let toggleBtn = 0; toggleBtn < toggleBtns.length; toggleBtn++) {
987
+ // Add tabindex
988
+ toggleBtns[toggleBtn].setAttribute("tabindex", "0");
989
+ // Show/hide on click
990
+ toggleBtns[toggleBtn].addEventListener("click", () => {
991
+ if (toggleBtns[toggleBtn].nextElementSibling) {
992
+ toggleBtns[toggleBtn].nextElementSibling.classList.toggle("show");
993
+ }
994
+ })
995
+
996
+ // Show/hide on enter for users who use tab
997
+ toggleBtns[toggleBtn].addEventListener("keydown", (enter) => {
998
+ if (enter.keyCode === 13) {
999
+ if (toggleBtns[toggleBtn].nextElementSibling) {
1000
+ toggleBtns[toggleBtn].nextElementSibling.classList.toggle("show");
1001
+ }
1002
+ }
1003
+ })
1004
+ }
1005
+ }
1006
+ }