// 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/>.
/**
* Element overlay methods.
*
* This module is used to create overlay information on components. For example
* to generate or destroy file drop-zones.
*
* @module core/local/reactive/overlay
* @copyright 2022 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Prefetch from 'core/prefetch';
// Prefetch the overlay html.
const overlayTemplate = 'core/local/reactive/overlay';
Prefetch.prefetchTemplate(overlayTemplate);
/**
* @var {boolean} isInitialized if the module is capturing the proper page events.
*/
let isInitialized = false;
/**
* @var {Object} isInitialized if the module is capturing the proper page events.
*/
const selectors = {
OVERLAY: "[data-overlay]",
REPOSITION: "[data-overlay-dynamic]",
NAVBAR: "nav.navbar.fixed-top",
};
/**
* Adds an overlay to a specific page element.
*
* @param {Object} definition the overlay definition.
* @param {String|Promise} definition.content an optional overlay content.
* @param {String|Promise} definition.icon an optional icon content.
* @param {String} definition.classes an optional CSS classes
* @param {HTMLElement} parent the parent object
* @return {HTMLElement|undefined} the new page element.
*/
export const addOverlay = async(definition, parent) => {
// Validate non of the passed params is a promise.
if (definition.content && typeof definition.content !== 'string') {
definition.content = await definition.content;
}
if (definition.icon && typeof definition.icon !== 'string') {
definition.icon = await definition.icon;
}
const data = {
content: definition.content,
css: definition.classes ?? 'file-drop-zone',
};
const {html, js} = await Templates.renderForPromise(overlayTemplate, data);
Templates.appendNodeContents(parent, html, js);
const overlay = parent.querySelector(selectors.OVERLAY);
rePositionPreviewInfoElement(overlay);
init();
return overlay;
};
/**
* Adds an overlay to a specific page element.
*
* @param {HTMLElement} overlay the parent object
*/
export const removeOverlay = (overlay) => {
if (!overlay || !overlay.parentNode) {
return;
}
// Remove any forced parentNode position.
if (overlay.dataset?.overlayPosition) {
delete overlay.parentNode.style.position;
}
overlay.parentNode.removeChild(overlay);
};
export const removeAllOverlays = () => {
document.querySelectorAll(selectors.OVERLAY).forEach(
(overlay) => {
removeOverlay(overlay);
}
);
};
/**
* Re-position the preview information element by calculating the section position.
*
* @param {Object} overlay the overlay element.
*/
const rePositionPreviewInfoElement = function(overlay) {
if (!overlay) {
throw new Error('Inexistent overlay element');
}
// Add relative position to the parent object.
if (!overlay.parentNode?.style?.position) {
overlay.parentNode.style.position = 'relative';
overlay.dataset.overlayPosition = "true";
}
// Get the element to reposition.
const target = overlay.querySelector(selectors.REPOSITION);
if (!target) {
return;
}
// Get the new bounds.
const rect = overlay.getBoundingClientRect();
const sectionHeight = parseInt(window.getComputedStyle(overlay).height, 10);
const sectionOffset = rect.top;
const previewHeight = parseInt(window.getComputedStyle(target).height, 10) +
(2 * parseInt(window.getComputedStyle(target).padding, 10));
// Calculate the new target position.
let top, bottom;
if (sectionOffset < 0) {
if (sectionHeight + sectionOffset >= previewHeight) {
// We have enough space here, just stick the preview to the top.
let offSetTop = 0 - sectionOffset;
const navBar = document.querySelector(selectors.NAVBAR);
if (navBar) {
offSetTop = offSetTop + navBar.offsetHeight;
}
top = offSetTop + 'px';
bottom = 'unset';
} else {
// We do not have enough space here, just stick the preview to the bottom.
top = 'unset';
bottom = 0;
}
} else {
top = 0;
bottom = 'unset';
}
target.style.top = top;
target.style.bottom = bottom;
};
// Update overlays when the page scrolls.
const init = () => {
if (isInitialized) {
return;
}
// Add scroll events.
document.addEventListener('scroll', () => {
document.querySelectorAll(selectors.OVERLAY).forEach(
(overlay) => {
rePositionPreviewInfoElement(overlay);
}
);
}, true);
};