// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Scroll manager is a class that help with saving the scroll positing when you
* click on an action icon, and then when the page is reloaded after processing
* the action, it scrolls you to exactly where you were. This is much nicer for
* the user.
*
* To use this in your code, you need to ensure that:
* 1. The button that triggers the action has to have a click event handler that
* calls saveScrollPos()
* 2. After doing the processing, the redirect() function will add 'mdlscrollto'
* parameter into the redirect url automatically.
* 3. Finally, on the page that is reloaded (which should be the same as the one
* the user started on) you need to call scrollToSavedPosition()
* on page load.
*
* @module core/scroll_manager
* @copyright 2021 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/** @property {HTMLElement} scrollingElement the current scrolling element. */
let scrollingElement = null;
/**
* Is the element scrollable?
*
* @param {HTMLElement} element Element.
* @returns {boolean}
*/
const isScrollable = (element) => {
// Check if the element has scrollable content.
const hasScrollableContent = element.scrollHeight > element.clientHeight;
// If 'overflow-y' is set to hidden, the scroll bar is't show.
const elementOverflow = window.getComputedStyle(element).overflowY;
const isOverflowHidden = elementOverflow.indexOf('hidden') !== -1;
return hasScrollableContent && !isOverflowHidden;
};
/**
* Get the scrolling element.
*
* @returns {HTMLElement}
*/
const getScrollingElement = () => {
if (scrollingElement === null) {
const page = document.getElementById('page');
if (isScrollable(page)) {
scrollingElement = page;
} else {
scrollingElement = document.scrollingElement;
}
}
return scrollingElement;
};
/**
* Get current scroll position.
*
* @returns {Number} Scroll position.
*/
const getScrollPos = () => {
const scrollingElement = getScrollingElement();
return scrollingElement.scrollTop;
};
/**
* Get the scroll position for this form.
*
* @param {HTMLFormElement} form
* @returns {HTMLInputElement}
*/
const getScrollPositionElement = (form) => {
const element = form.querySelector('input[name=mdlscrollto]');
if (element) {
return element;
}
const scrollPos = document.createElement('input');
scrollPos.type = 'hidden';
scrollPos.name = 'mdlscrollto';
form.appendChild(scrollPos);
return scrollPos;
};
/**
* In the form that contains the element, set the value of the form field with
* name mdlscrollto to the current scroll position. If there is no element with
* that name, it creates a hidden form field with that name within the form.
*
* @param {string} elementId The element in the form.
*/
export const saveScrollPos = (elementId) => {
const element = document.getElementById(elementId);
const form = element.closest('form');
if (!form) {
return;
}
saveScrollPositionToForm(form);
};
/**
* Init event handlers for all links with data-savescrollposition=true.
* Set the value to the closest form.
*/
export const watchScrollButtonSaves = () => {
document.addEventListener('click', (e) => {
const button = e.target.closest('[data-savescrollposition="true"]');
if (button) {
saveScrollPositionToForm(button.form);
}
});
};
/**
* Save the position to form.
*
* @param {Object} form The form is saved scroll position.
*/
export const saveScrollPositionToForm = (form) => {
getScrollPositionElement(form).value = getScrollPos();
};
/**
* Init event handlers for all links with data-save-scroll=true.
* Handle to add mdlscrollto parameter to link using js when we click on the link.
*
*/
export const initLinksScrollPos = () => {
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-save-scroll=true]');
if (!link) {
return;
}
e.preventDefault();
const url = new URL(e.target.href);
url.searchParams.set('mdlscrollto', getScrollPos());
window.location = url;
});
};
/**
* If there is a parameter like mdlscrollto=123 in the URL, scroll to that saved position.
*/
export const scrollToSavedPosition = () => {
const url = new URL(window.location.href);
if (!url.searchParams.has('mdlscrollto')) {
return;
}
const scrollPosition = url.searchParams.get('mdlscrollto');
// Event onDOMReady is the effective one here. I am leaving the immediate call to
// window.scrollTo in case it reduces flicker.
const scrollingElement = getScrollingElement();
scrollingElement.scrollTo(0, scrollPosition);
document.addEventListener('DOMContentLoaded', () => {
scrollingElement.scrollTo(0, scrollPosition);
});
};