1window.modalStack = [] 2window.viewStack = [] 3window.hidingSlidableModalInProgress = false 4 5// 6// Soft Keyboard Tracking 7// 8 9// The swipe-to-dismiss gesture below fires on any downward touchmove at all, which makes modals 10// far too easy to dismiss by accident while the soft keyboard is up. We track the viewport height 11// so that gesture can be turned off until the keyboard is put away. all/index.php declares 12// interactive-widget=resizes-content, so the viewport really does shrink when the keyboard appears 13// (and even under the default resizes-visual, visualViewport.height still shrinks). 14window.largestViewportHeightSeen = 0 15 16function getCurrentViewportHeight(){ 17 return (window.visualViewport && window.visualViewport.height) || window.innerHeight 18} 19 20// the tallest we've ever seen the viewport is our keyboard-is-down baseline. It only ever grows, 21// so a keyboard appearing can't poison it, and a keyboard going away restores it. 22function recordViewportHeight(){ 23 var height = getCurrentViewportHeight() 24 if(height >
24 window.largestViewportHeightSeen){ 25 window.largestViewportHeightSeen = height 26 } 27} 28 29// the keyboard counts as up once the viewport has shrunk well past what browser chrome alone could 30// explain. Android keyboards eat 250px+, while a URL bar sliding away is closer to 100px. 31function softKeyboardIsUp(){ 32 if(window.largestViewportHeightSeen == 0){ 33 return false 34 } 35 var shrinkage = window.largestViewportHeightSeen - getCurrentViewportHeight() 36 return shrinkage > Math.max(150, window.largestViewportHeightSeen * 0.15) 37} 38 39recordViewportHeight() 40if(window.visualViewport){ 41 window.visualViewport.addEventListener('resize', recordViewportHeight) 42} 43window.addEventListener('resize', recordViewportHeight) 44// a rotation means an entirely new baseline, so throw the old one out and remeasure once the 45// browser has settled on the new dimensions 46window.addEventListener('orientationchange', function(){ 47 window.largestViewportHeightSeen = 0 48 setTimeout(recordViewportHeight, 500) 49}) 50 51// Reintegrating hardware back button. Always called twice? 52window.addEventListener("popstate", function () { 53 if(window.shouldBlockUserOutOfUsingAndTellThemToGoToAppStore){return;} 54 window.history.pushState({}, '') 55 var thisScreen = window.viewStack[window.viewStack.length-1]; 56 if (thisScreen && thisScreen.includes("modal")){ 57 hideSlidableModal() 58 } 59}); 60 61function getZIndexOfHighestSlidableModal() { 62 let highest = 0; 63 $('.slideable-modal-visible').each(function() { 64 const z = parseInt($(this).css('z-index'), 10); 65 if (!isNaN(z) && z > highest) { 66 highest = z; 67 } 68 }); 69 return highest; 70} 71 72// 73// Modal Render Functions 74// 75 76// Loader function for slidable modal. Give the element id from the main web app for use in the mobile modal
77function renderSlidableModal(elementToInsert, fullHeight){ 78 // if there are not modals active 79 //this is not ideal; it would be good to get this in a while loop for arbitrary nested slidable modal depth. 80 if(window.modalStack.length < $('.slidableModal').length){ 81 // relocate all slidableModal contents to hiddenModal 82 var slidableModalLength = $('.slidableModal').length 83 $(`.slidableModal[index=${slidableModalLength}]`).children().appendTo($("#hiddenModals")); 84 // remove modals so that we can reuse indices 85 86 $(`.slidableModal[index=${slidableModalLength}]`).remove() 87 if (window.modalStack.length == 0) 88 { 89 $(`.slidableModal`).children().appendTo($("#hiddenModals")) 90 $(".slidableModal").remove(); 91 } 92 } 93 94 95 // wash classes to avoid conflicting attributes 96 $(elementToInsert).removeClass('modal') 97 $(elementToInsert).removeClass('fade') 98 99 // create the name of the modal that we're popping onto the stack 100 var screenName = "modal" + elementToInsert 101 window.modalStack.push(screenName) 102 window.viewStack.push(screenName) 103 104 105 106 107 if ($('.slidableModal[index="'+window.modalStack.length+'"]').length == 0) { 108 // create a new slidable modal that we put this content in 109 $("#shadowBackgroundBehindModals").before(` 110 <div class="slidableModal ${fullHeight ? 'heightVisible' : ''}" index="${window.modalStack.length}"> 111 </div> 112 `); 113 }else{ 114 //if the modal already exists, we don't want a stale view. Relocate its contents before reuse. 115 $('.slidableModal[index="'+window.modalStack.length+'"]').children().appendTo($("#hiddenModals")); 116 } 117 118 var newModal = $('.slidableModal[index="'+window.modalStack.length+'"]'); 119 newModal.attr('data-selector', elementToInsert); 120 121 122 123 $(elementToInsert).appendTo(newModal); 124 125 // now show the modal 126 newModal.scrollTop(0); // apparently this is fucking necessary, for the thing to animate 127 newModal.addClass("slideable-modal-visible"); 128 129 newModal.css('z-index', 9998); 130 newModal.css('z-index', getZIndexOfHighestSlidableModal() + 1); 131 $("#shadowBackgroundBehindModals").css('z-index', getZIndexOfHighestSlidableModal()
131- 1) 132 133 $("#shadowBackgroundBehindModals").addClass("shadow-background-visible"); 134 135 //don't allow swipe on main school selection page 136 if(!$(elementToInsert).hasClass("unswipeable")) 137 { 138 // attach swipe so that we can dismiss them 139 newModal[0].addEventListener('touchstart', handleTouchStart, false); 140 newModal[0].addEventListener('touchmove', handleTouchMove, false); 141 newModal[0].addEventListener('touchend', handleTouchEnd, false); 142 newModal[0].addEventListener('touchcancel', handleTouchEnd, false); 143 $("#shadowBackgroundBehindModals")[0].addEventListener('touchstart', handleTouchStart, false); 144 $("#shadowBackgroundBehindModals")[0].addEventListener('touchmove', handleTouchMove, false); 145 $("#shadowBackgroundBehindModals")[0].addEventListener('touchend', handleTouchEnd, false); 146 $("#shadowBackgroundBehindModals")[0].addEventListener('touchcancel', handleTouchEnd, false); 147 148 // how far down the finger has to travel before the swipe counts as "dismiss this". Before this 149 // existed the gesture fired on the first touchmove event carrying any downward movement at all â 150 // a pixel or two â which is what made these sheets feel like they closed the moment you touched 151 // them. Note this is measured from where the finger first landed, so it's the whole gesture's 152 // travel, not the distance between two consecutive touchmove events. 153 const SWIPE_TO_DISMISS_DISTANCE = 35; 154 // with the soft keyboard up, the swipe additionally has to START in the band at the top of the 155 // sheet. Everything below that band is inches from the keyboard and covered in fields someone is 156 // actively typing into. 157 const GRAB_BAND_PORTION_OF_MODAL = 0.10; 158 // ...but never smaller than a fingertip. The band is a share of a sheet the keyboard has already 159 // squeezed (the app viewport shrinks, and .slidableModal is max-height: 93% of it), so a flat 10% 160 // would land under Android's 48dp minimum touch target exactly when the band is the only way out. 161 const GRAB_BAND_MINIMUM_HEIGHT = 60; 162 163 var xDown = null; 164 var yDown = null; 165 var startedInGrabBand = false; 166 167 function getTouches(evt) { 168 return evt.touches || // browser API 169 evt.originalEvent.touches; // jQuery 170 } 171 172 // the sheet is position: fixed, so its top edge is a stable viewport coordinate even while the 173 // content inside it scrolls. A touch on the shadow background lands above that edge and so counts 174 // as in the band, which is what we want â swiping the dimmed area should still dismiss. 175 function touchIsInGrabBand(y) { 176 const modalTop = newModal[0].getBoundingClientRect().top; 177 const bandHeight = Math.max(GRAB_BAND_MINIMUM_HEIGHT, newModal[0].offsetHeight * GRAB_BAND_PORTION_OF_MODAL); 178 return (y - modalTop) < bandHeight; 179 } 180 181 function handleTouchStart(evt) { 182 const firstTouch = getTouches(evt)[0]; 183 xDown = firstTouch.clientX; 184 yDown = firstTouch.clientY; 185 // where the finger LANDED is what decides this, not where it happens to be partway through the 186 // drag, so it's settled here and then left alone for the rest of the gesture 187 startedInGrabBand = touchIsInGrabBand(yDown); 188 }; 189 190 // a finished gesture shouldn't leave its start point lying around for the next one to measure from 191 function handleTouchEnd() { 192 xDown = null; 193 yDown = null; 194 startedInGrabBand = false; 195 }; 196 197 //elementToInsert : scrollableElement 198 elements = { 199 "#calSettingsModal":"#calSchedulesListContainer", 200 "#postViewCommentsAndInputContainer" : "#postViewAndCommentsContainer", 201 "#addClassModal" : "#classSearchResultsContainer", 202 "#createCustomEventModal" : "#createCustomEventModal", 203 "#createCustomClassModal":"#createCustomClassModal", 204 '#createTaskModal':'#createTaskModal', 205 '#addChatsModal': '#addChatsModal', 206 '#eventDetailView': '#eventDetailView', 207 "#scheduleNavModal": '#scheduleNavModal', 208 '#taskFilterModal': '#taskFilterModal', 209 '#infoColumn': '#infoColumn', 210 '#allDayEventModal': '#allDayEventModal',
211 '#taskDetailsModal': '#taskDetailsModal', 212 '#chatProfileChangeSchool': '#universalSchoolSearchResultsContainer', 213 '#chatProfileChangeMajor': '#chatProfileChangeMajorContainer', 214 '#newPremiumModal': '#newPremiumModalScrollContainer', 215 '#themeSelectorModal': '.themeSelectorScrollContent, #allThemesSubpanelContent, #allFXSubpanelContent', 216 '#setAsDefaultRemindersModal': '#setAsDefaultRemindersModal', 217 '#syllabusScanModal': '#syllabusScanResults' 218 219 } 220 function handleTouchMove(evt) { 221 222 // list of exclusions whene, if they swipe on this, it shouldn't affect things 223 var exclusions = ["cropper-drag-box", "cropper-face", "cropper-line", "dropdown-option"]; 224 for(index in exclusions){ 225 let exclusion = exclusions[index] 226 if($(evt.target).hasClass(exclusion)){ 227 return 228 } 229 } 230 231 if ( ! xDown || ! yDown ) { 232 return; 233 } 234 var xUp = evt.touches[0].clientX; 235 var yUp = evt.touches[0].clientY; 236 237 // both measured from the touchstart, so they accumulate over the gesture rather than resetting 238 // on every event the way they used to 239 var xDiff = xDown - xUp; 240 var yDiff = yDown - yUp; 241 242 if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/ 243 // if it's horizontal 244 } else { 245 // if it's vertical and it's down, and it has gone far enough to be a deliberate drag rather 246 // than the shake of a finger doing something else 247 if ( yDiff < 0 && Math.abs( yDiff ) >= SWIPE_TO_DISMISS_DISTANCE && ( !softKeyboardIsUp() || startedInGrabBand ) ) { 248 // only if they're swiping down and the modal is scrolled to the top already, then dismiss 249 //there are a number of modals for which we would like to only dismiss on scroll 250 //if the modal has reached the top. The problem with making this universal is that 251 //there is no one standard element to check for scrolltop == 0. Sometimes its the modal 252 //itself, sometimes it's some container therein. Thus the dictionary structure is necessary. 253 if($(elementToInsert).attr('id') && Object.keys(elements).includes('#' + $(elementToInsert).attr('id'))){ 254 let elementToInsertID = elements['#' + $(elementToInsert).attr('id')] 255 let scrollableDivs = $(elementToInsertID) 256 // Check if ANY of the matched elements are scrolled - if so, don't dismiss 257 let anyScrolled = false 258 scrollableDivs.each(function() { 259 if ($(this).scrollTop() > 0) { 260 anyScrolled = true 261 return false // break out of each loop 262 } 263 }) 264 if (!anyScrolled) { 265 hideSlidableModal() 266 } 267 }else{ 268 hideSlidableModal() 269 } 270 } 271 } 272 273 /* deliberately NOT resetting xDown/yDown here. They're the origin the distance above is 274 measured against, so clearing them every event is what limited the old check to the gap 275 between two consecutive touchmoves â a pixel or two, and no way to require a real drag. 276 handleTouchEnd clears them when the finger actually leaves. */ 277 }; 278 } 279 280 // removed the code for tracking a swipe because it was just too complicated for now 281 /* 282 // still need to calculate containment 283 // init it as draggable, only up and down via y axis, but start disabled 284 var distanceFromTop = $(window).height() - newModal.height(); 285 newModal.draggable({ 286 containment: [0, distanceFromTop, $(window).width(), Infinity], 287 axis: 'y', 288 }); 289 newModal.draggable('disable') 290 291 // key track of eache drag, check to see if it's up or down 292 var lastY; 293 newModal.bind('touchmove', function (e){ 294 var currentY = e.originalEvent.touches[0].clientY; 295 // a drag moving down 296 if(currentY > lastY){ 297 // if they're already at the top of the modal, then we should allow the dragging 298 // of the modal to happen, so we enable it. We only do this if it's disabled so it's not called a bunch 299 if($(this).scrollTop() == 0 && newModal.draggable('option', 'disabled')){ 300 $(this).draggable( 'enable' ) 301 } 302 // it's a drag up 303 }else if(currentY < lastY){ 304 // in every case, a drag up won't be able to dismiss the modal, so we disable its draggability 305 $(this).draggable( 'disable' ) 306 } 307 lastY = currentY; 308 }); 309 310 // if they scroll all the way to the top, then we want to re-enable the dragging 311 newModal.on( 'scroll', function(){ 312 if($(this).scrollTop() == 0){ 313 $(this).draggable( 'enable' ) 314 } 315 }); 316 */ 317 318} 319
320// close current modal - if clear stack is true then close all modals in stack 321function hideSlidableModal(clearStack, redirect = false, closeConfirmationText = null) { 322 // ensures we don't just give up in the below for loop. 323 if(window.hidingSlidableModalInProgress){ 324 return 325 } 326 327 if (clearStack == true) { 328 for (var i = 0; i < window.modalStack.length; i++) { 329 hideSlidableModal() 330 } 331 } 332 var modal = $('.slidableModal[index="'+window.modalStack.length+'"]'); 333 334 if (modal.data("closeconfirmationmessage") != null){ 335 if(!confirm(modal.data("closeconfirmationmessage"))){ 336 return 337 } 338 } 339 340 if(modal.hasClass("unswipeable")){ 341 return 342 } 343 344 345 if(window.modalStack.length == 1){ 346 $("#shadowBackgroundBehindModals").removeClass("shadow-background-visible") 347 } 348 349 $("#shadowBackgroundBehindModals").css('z-index', Math.min(9998, getZIndexOfHighestSlidableModal() - 1)) 350 modal.removeClass("slideable-modal-visible"); 351 352 $(document).trigger("slidableModalHidden") 353 354 355 //sometimes its expedient to also know the actual id that was hidden if available 356 let idOfModalClosed = modal.children().first().attr('id') 357 if(idOfModalClosed){ 358 $(document).trigger(`${idOfModalClosed}.modalHidden`) 359 } 360 // it's hiding, so to prevent multiple calls to slide modal due to their finger, we temporarily delay future calls to hideSlidableModal 361 window.hidingSlidableModalInProgress = true 362 setTimeout(function(){ 363 window.hidingSlidableModalInProgress = false; 364 }, 550); // needs to match transition duration 365 366 // couldn't get this way to work, but wouldn't use the animation. Resulted in a race condition on user input 367 /* var modalGuts = modal.clone(); 368 modalGuts.appendTo($("#hiddenModals")); 369 // wait until the animation is done before putting the HTML back, otherwise the modal just goes blank 370 setTimeout(function(){ 371 modal.remove(); 372 }, 4000); 373 */ 374 window.modalStack.pop(); 375}
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.