PageSourceSearch

https://www.coursicle.com/shared/js/slidableModal.js?new11

js coursicle.com collected 2026-09-24 08:48:12 UTC 15,083 bytes, 375 lines download raw bytes

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.