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:
Ferran Recio 2022-11-30 15:59:31 +01:00
parent 2846751f2b
commit e359b9889a
13 changed files with 406 additions and 12 deletions

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View File

@ -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();
}
}

View File

@ -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'
);
});
}
}

View 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);
};

View 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>

View File

@ -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;
}

View File

@ -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; }

View File

@ -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; }