mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 00:42:54 +02:00
MDL-76432 core: reactive drapdrop file support
This commit adds all the necessary CSS and logic to handle file dropping into a reactive compoment. From now on, a reactive application can handle both element drag&drop and file drop easily.
This commit is contained in:
parent
2846751f2b
commit
e359b9889a
@ -1,4 +1,4 @@
|
||||
define("core/local/reactive/basecomponent",["exports","core/templates"],(function(_exports,_templates){var obj;
|
||||
define("core/local/reactive/basecomponent",["exports","core/templates","core/local/reactive/overlay"],(function(_exports,_templates,_overlay){var obj;
|
||||
/**
|
||||
* Reactive UI component base class.
|
||||
*
|
||||
@ -8,6 +8,6 @@ define("core/local/reactive/basecomponent",["exports","core/templates"],(functio
|
||||
* @class core/local/reactive/basecomponent
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}},_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(descriptor){if(void 0===descriptor.element||!(descriptor.element instanceof HTMLElement))throw Error("Reactive components needs a main DOM element to dispatch events");this.element=descriptor.element,this.eventHandlers=new Map([]),this.eventListeners=[],this.selectors={},this.events=this.constructor.getEvents(),this.create(descriptor),void 0!==descriptor.selectors&&this.addSelectors(descriptor.selectors),void 0===descriptor.reactive?this.element.dispatchEvent(new CustomEvent("core/reactive:requestRegistration",{bubbles:!0,detail:{component:this}})):(this.reactive=descriptor.reactive,this.reactive.registerComponent(this),this.addEventListener(this.element,"core/reactive:requestRegistration",(event=>{var _event$detail;null!=event&&null!==(_event$detail=event.detail)&&void 0!==_event$detail&&_event$detail.component&&(event.stopPropagation(),this.registerChildComponent(event.detail.component))})))}static getEvents(){return{}}create(descriptor){}destroy(){}getWatchers(){return[]}stateReady(){}getElement(query,dataId){if(void 0===query&&void 0===dataId)return this.element;const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelector(selector)}getElements(query,dataId){const dataSelector=dataId?"[data-id='".concat(dataId,"']"):"",selector="".concat(null!=query?query:"").concat(dataSelector);return this.element.querySelectorAll(selector)}addSelectors(newSelectors){for(const[selectorName,selector]of Object.entries(newSelectors))this.selectors[selectorName]=selector}getSelector(selectorName){return this.selectors[selectorName]}dispatchEvent(eventName,detail){this.element.dispatchEvent(new CustomEvent(eventName,{bubbles:!0,detail:detail}))}renderComponent(target,file,data){return new Promise(((resolve,reject)=>{target.addEventListener("ComponentRegistration:Success",(_ref=>{let{detail:detail}=_ref;resolve(detail.component)})),target.addEventListener("ComponentRegistration:Fail",(()=>{reject("Registration of ".concat(file," fails."))})),_templates.default.renderForPromise(file,data).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNodeContents(target,html,js),!0})).catch((error=>{throw reject("Rendering of ".concat(file," throws an error.")),error}))}))}addEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0===bindListener&&(bindListener=listener.bind(this),this.eventHandlers.set(listener,bindListener)),target.addEventListener(type,bindListener),this.eventListeners.push({target:target,type:type,bindListener:bindListener})}removeEventListener(target,type,listener){let bindListener=this.eventHandlers.get(listener);void 0!==bindListener&&target.removeEventListener(type,bindListener)}removeAllEventListeners(){this.eventListeners.forEach((_ref3=>{let{target:target,type:type,bindListener:bindListener}=_ref3;target.removeEventListener(type,bindListener)})),this.eventListeners=[]}remove(){this.unregister(),this.element.remove()}unregister(){this.reactive.unregisterComponent(this),this.removeAllEventListeners(),this.destroy()}dispatchRegistrationSuccess(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Success",{bubbles:!1,detail:{component:this}}))}dispatchRegistrationFail(){void 0!==this.element.parentNode&&this.element.parentNode.dispatchEvent(new CustomEvent("ComponentRegistration:Fail",{bubbles:!1,detail:{component:this}}))}registerChildComponent(component){component.reactive=this.reactive,this.reactive.registerComponent(component)}set locked(locked){this.setElementLocked(this.element,locked)}get locked(){return this.getElementLocked(this.element)}setElementLocked(target,locked){target.dataset.locked=null!=locked&&locked,locked?(target.style.pointerEvents="none",target.style.userSelect="none",target.hasAttribute("draggable")&&target.setAttribute("draggable",!1),target.setAttribute("aria-busy",!0)):(target.style.pointerEvents=null,target.style.userSelect=null,target.hasAttribute("draggable")&&target.setAttribute("draggable",!0),target.setAttribute("aria-busy",!1))}getElementLocked(target){var _target$dataset$locke;return null!==(_target$dataset$locke=target.dataset.locked)&&void 0!==_target$dataset$locke&&_target$dataset$locke}async addOverlay(definition,target){var _definition$classes;this._overlay&&this.removeOverlay(),this._overlay=await(0,_overlay.addOverlay)({content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"},null!=target?target:this.element)}removeOverlay(){this._overlay&&((0,_overlay.removeOverlay)(this._overlay),this._overlay=null)}removeAllOverlays(){(0,_overlay.removeAllOverlays)()}},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=basecomponent.min.js.map
|
File diff suppressed because one or more lines are too long
2
lib/amd/build/local/reactive/dragdrop.min.js
vendored
2
lib/amd/build/local/reactive/dragdrop.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
lib/amd/build/local/reactive/overlay.min.js
vendored
Normal file
13
lib/amd/build/local/reactive/overlay.min.js
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
define("core/local/reactive/overlay",["exports","core/templates","core/prefetch"],(function(_exports,_templates,_prefetch){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
|
||||
/**
|
||||
* 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
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.removeOverlay=_exports.removeAllOverlays=_exports.addOverlay=void 0,_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch);_prefetch.default.prefetchTemplate("core/local/reactive/overlay");const selectors_OVERLAY="[data-overlay]",selectors_REPOSITION="[data-overlay-dynamic]",selectors_NAVBAR="nav.navbar.fixed-top";_exports.addOverlay=async(definition,parent)=>{var _definition$classes;definition.content&&"string"!=typeof definition.content&&(definition.content=await definition.content),definition.icon&&"string"!=typeof definition.icon&&(definition.icon=await definition.icon);const data={content:definition.content,css:null!==(_definition$classes=definition.classes)&&void 0!==_definition$classes?_definition$classes:"file-drop-zone"};let overlay;try{const{html:html,js:js}=await _templates.default.renderForPromise("core/local/reactive/overlay",data);_templates.default.appendNodeContents(parent,html,js),overlay=parent.querySelector(selectors_OVERLAY),rePositionPreviewInfoElement(overlay),init()}catch(error){throw error}return overlay};const removeOverlay=overlay=>{var _overlay$dataset;overlay&&overlay.parentNode&&(null!==(_overlay$dataset=overlay.dataset)&&void 0!==_overlay$dataset&&_overlay$dataset.overlayPosition&&delete overlay.parentNode.style.position,overlay.parentNode.removeChild(overlay))};_exports.removeOverlay=removeOverlay;_exports.removeAllOverlays=()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{removeOverlay(overlay)}))};const rePositionPreviewInfoElement=function(overlay){var _overlay$parentNode,_overlay$parentNode$s;if(!overlay)throw new Error("Inexistent overlay element");null!==(_overlay$parentNode=overlay.parentNode)&&void 0!==_overlay$parentNode&&null!==(_overlay$parentNode$s=_overlay$parentNode.style)&&void 0!==_overlay$parentNode$s&&_overlay$parentNode$s.position||(overlay.parentNode.style.position="relative",overlay.dataset.overlayPosition="true");const target=overlay.querySelector(selectors_REPOSITION);if(!target)return;const rect=overlay.getBoundingClientRect(),sectionHeight=parseInt(window.getComputedStyle(overlay).height,10),sectionOffset=rect.top,previewHeight=parseInt(window.getComputedStyle(target).height,10)+2*parseInt(window.getComputedStyle(target).padding,10);let top,bottom;if(sectionOffset<0)if(sectionHeight+sectionOffset>=previewHeight){let offSetTop=0-sectionOffset;const navBar=document.querySelector(selectors_NAVBAR);navBar&&(offSetTop+=navBar.offsetHeight),top=offSetTop+"px",bottom="unset"}else top="unset",bottom=0;else top=0,bottom="unset";target.style.top=top,target.style.bottom=bottom},init=()=>{document.addEventListener("scroll",(()=>{document.querySelectorAll(selectors_OVERLAY).forEach((overlay=>{rePositionPreviewInfoElement(overlay)}))}),!0)}}));
|
||||
|
||||
//# sourceMappingURL=overlay.min.js.map
|
1
lib/amd/build/local/reactive/overlay.min.js.map
Normal file
1
lib/amd/build/local/reactive/overlay.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -14,6 +14,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Templates from 'core/templates';
|
||||
import {addOverlay, removeOverlay, removeAllOverlays} from 'core/local/reactive/overlay';
|
||||
|
||||
/**
|
||||
* Reactive UI component base class.
|
||||
@ -488,4 +489,43 @@ export default class {
|
||||
getElementLocked(target) {
|
||||
return target.dataset.locked ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an overlay to a specific page element.
|
||||
*
|
||||
* @param {Object} definition the overlay definition.
|
||||
* @param {String} definition.content an optional overlay content.
|
||||
* @param {String} definition.classes an optional CSS classes
|
||||
* @param {Element} target optional parent object (this.element will be used if none provided)
|
||||
*/
|
||||
async addOverlay(definition, target) {
|
||||
if (this._overlay) {
|
||||
this.removeOverlay();
|
||||
}
|
||||
this._overlay = await addOverlay(
|
||||
{
|
||||
content: definition.content,
|
||||
css: definition.classes ?? 'file-drop-zone',
|
||||
},
|
||||
target ?? this.element
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the current overlay.
|
||||
*/
|
||||
removeOverlay() {
|
||||
if (!this._overlay) {
|
||||
return;
|
||||
}
|
||||
removeOverlay(this._overlay);
|
||||
this._overlay = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all page overlais.
|
||||
*/
|
||||
removeAllOverlays() {
|
||||
removeAllOverlays();
|
||||
}
|
||||
}
|
||||
|
54
lib/amd/src/local/reactive/dragdrop.js
vendored
54
lib/amd/src/local/reactive/dragdrop.js
vendored
@ -147,6 +147,8 @@ export default class extends BaseComponent {
|
||||
// Stores if the droparea is shown or not.
|
||||
this.dropzonevisible = false;
|
||||
|
||||
// Stores if the mouse is over the element or not.
|
||||
this.ismouseover = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,6 +160,15 @@ export default class extends BaseComponent {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current drop-zone visible of the element.
|
||||
*
|
||||
* @returns {boolean} if the dropzone should be visible or not
|
||||
*/
|
||||
isDropzoneVisible() {
|
||||
return this.dropzonevisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state ready method.
|
||||
*
|
||||
@ -174,6 +185,8 @@ export default class extends BaseComponent {
|
||||
this.addEventListener(this.element, 'dragleave', this._dragLeave);
|
||||
this.addEventListener(this.element, 'dragover', this._dragOver);
|
||||
this.addEventListener(this.element, 'drop', this._drop);
|
||||
this.addEventListener(this.element, 'mouseover', this._mouseOver);
|
||||
this.addEventListener(this.element, 'mouseleave', this._mouseLeave);
|
||||
}
|
||||
|
||||
// Configure the elements draggable if the parent component has dragable data.
|
||||
@ -203,6 +216,20 @@ export default class extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse over handle.
|
||||
*/
|
||||
_mouseOver() {
|
||||
this.ismouseover = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse leave handler.
|
||||
*/
|
||||
_mouseLeave() {
|
||||
this.ismouseover = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag start event handler.
|
||||
*
|
||||
@ -278,6 +305,7 @@ export default class extends BaseComponent {
|
||||
document.body.classList.remove(this.classes.BODYDRAGGING);
|
||||
this.element.classList.remove(this.classes.DRAGGING);
|
||||
this.fullregion?.classList.remove(this.classes.DRAGGING);
|
||||
this.removeAllOverlays();
|
||||
|
||||
// We add the total movement to the event in case the component
|
||||
// wants to move its absolute position.
|
||||
@ -339,7 +367,7 @@ export default class extends BaseComponent {
|
||||
const dropdata = this._processEvent(event);
|
||||
if (dropdata) {
|
||||
this.entercount--;
|
||||
if (this.entercount == 0 && this.dropzonevisible) {
|
||||
if (this.entercount <= 0 && this.dropzonevisible) {
|
||||
this.dropzonevisible = false;
|
||||
this.element.classList.remove(this.classes.DRAGOVER);
|
||||
this._callParentMethod('hideDropZone', dropdata, event);
|
||||
@ -363,6 +391,7 @@ export default class extends BaseComponent {
|
||||
this._callParentMethod('hideDropZone', dropdata, event);
|
||||
}
|
||||
this.element.classList.remove(this.classes.DRAGOVER);
|
||||
this.removeAllOverlays();
|
||||
this._callParentMethod('drop', dropdata, event);
|
||||
// An accepted drop resets the initial position.
|
||||
// Save the starting point.
|
||||
@ -443,7 +472,12 @@ export default class extends BaseComponent {
|
||||
* @returns {Object|undefined} with the dragged data (or undefined if none)
|
||||
*/
|
||||
_getDropData(event) {
|
||||
if (this._containsOnlyFiles(event)) {
|
||||
this._isOnlyFilesDragging = this._containsOnlyFiles(event);
|
||||
if (this._isOnlyFilesDragging) {
|
||||
// Check if the reactive instance can provide a files draggable data.
|
||||
if (this.reactive.getFilesDraggableData !== undefined && typeof this.reactive.getFilesDraggableData === 'function') {
|
||||
return this.reactive.getFilesDraggableData(event.dataTransfer);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return activeDropData.get(this.reactive);
|
||||
@ -455,15 +489,21 @@ export default class extends BaseComponent {
|
||||
* Files dragging does not generate drop data because they came from outside the page and the component
|
||||
* must check it before validating the event.
|
||||
*
|
||||
* Some browsers like Firefox add extra types to file dragging. To discard the false positives
|
||||
* a double check is necessary.
|
||||
*
|
||||
* @param {Event} event the original event.
|
||||
* @returns {boolean} if the drag dataTransfers contains files.
|
||||
*/
|
||||
_containsOnlyFiles(event) {
|
||||
if (event.dataTransfer.types && event.dataTransfer.types.length > 0) {
|
||||
// Chrome drag page images as files. To differentiate a real file from a page
|
||||
// image we need to check if all the dataTransfers types are files.
|
||||
return event.dataTransfer.types.every(type => type === 'Files');
|
||||
if (!event.dataTransfer.types.includes('Files')) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return event.dataTransfer.types.every((type) => {
|
||||
return (type.toLowerCase() != 'text/uri-list'
|
||||
&& type.toLowerCase() != 'text/html'
|
||||
&& type.toLowerCase() != 'text/plain'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
171
lib/amd/src/local/reactive/overlay.js
Normal file
171
lib/amd/src/local/reactive/overlay.js
Normal file
@ -0,0 +1,171 @@
|
||||
// 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',
|
||||
};
|
||||
let overlay;
|
||||
try {
|
||||
const {html, js} = await Templates.renderForPromise(overlayTemplate, data);
|
||||
Templates.appendNodeContents(parent, html, js);
|
||||
overlay = parent.querySelector(selectors.OVERLAY);
|
||||
rePositionPreviewInfoElement(overlay);
|
||||
init();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
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);
|
||||
};
|
36
lib/templates/local/reactive/overlay.mustache
Normal file
36
lib/templates/local/reactive/overlay.mustache
Normal file
@ -0,0 +1,36 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/local/reactive/overlay
|
||||
Template to render the global reactive debug panel.
|
||||
Classes required for JS:
|
||||
* none
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
Example context (json):
|
||||
{
|
||||
"content": "Drop here!",
|
||||
"icon": "<i class='icon fa fa-question-circle'></i>"
|
||||
}
|
||||
}}
|
||||
<div class="overlay-preview" data-overlay="true">
|
||||
{{#content}}
|
||||
<div class="overlay-preview-wrapper" data-overlay-dynamic="true">
|
||||
<div class="overlay-preview-content">
|
||||
{{{icon}}}
|
||||
{{content}}
|
||||
</div>
|
||||
</div>
|
||||
{{/content}}
|
||||
</div>
|
@ -2909,6 +2909,41 @@ body.dragging {
|
||||
|
||||
// Generic classes reactive components can use.
|
||||
|
||||
.overlay-preview {
|
||||
background-color: rgba($white, .8);
|
||||
border: 2px dashed $primary;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.overlay-preview-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.overlay-preview-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
padding: $modal-inner-padding;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-size: $font-size-lg;
|
||||
@include border-radius();
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-preview-borders {
|
||||
outline: 2px dashed $primary;
|
||||
}
|
||||
|
||||
.waitstate {
|
||||
display: none;
|
||||
}
|
||||
|
@ -12228,6 +12228,35 @@ body.dragging .dragging {
|
||||
visibility: visible;
|
||||
cursor: move; }
|
||||
|
||||
.overlay-preview {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 2px dashed #0f6cbf;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; }
|
||||
.overlay-preview .overlay-preview-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 2rem;
|
||||
width: 100%; }
|
||||
.overlay-preview .overlay-preview-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background-color: #0f6cbf;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 1.171875rem;
|
||||
border-radius: 0.5rem; }
|
||||
|
||||
.overlay-preview-borders {
|
||||
outline: 2px dashed #0f6cbf; }
|
||||
|
||||
.waitstate {
|
||||
display: none; }
|
||||
|
||||
|
@ -12228,6 +12228,35 @@ body.dragging .dragging {
|
||||
visibility: visible;
|
||||
cursor: move; }
|
||||
|
||||
.overlay-preview {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 2px dashed #0f6cbf;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; }
|
||||
.overlay-preview .overlay-preview-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 2rem;
|
||||
width: 100%; }
|
||||
.overlay-preview .overlay-preview-content {
|
||||
position: relative;
|
||||
top: 0;
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background-color: #0f6cbf;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 1.171875rem;
|
||||
border-radius: 0.25rem; }
|
||||
|
||||
.overlay-preview-borders {
|
||||
outline: 2px dashed #0f6cbf; }
|
||||
|
||||
.waitstate {
|
||||
display: none; }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user