From 0d4069918e28b3e325a0d9438e879dc17001778e Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Mon, 21 Feb 2022 15:08:42 +0700 Subject: [PATCH] MDL-73457 Course: Fix drag & drop image prevents moving resources Including in this commit: - Modify the UI to make the section dropzone more visible - Prevent the direct image dragging in Chrome, Safari by improving the check --- course/dndupload.js | 124 ++++++++++++++---- lang/en/moodle.php | 2 +- lib/amd/build/local/reactive/dragdrop.min.js | 2 +- .../build/local/reactive/dragdrop.min.js.map | 2 +- lib/amd/src/local/reactive/dragdrop.js | 18 ++- theme/boost/scss/moodle/course.scss | 37 +++++- theme/boost/style/moodle.css | 31 ++++- theme/classic/style/moodle.css | 31 ++++- 8 files changed, 197 insertions(+), 50 deletions(-) diff --git a/course/dndupload.js b/course/dndupload.js index 0f8ff98323b..0fcb900eff6 100644 --- a/course/dndupload.js +++ b/course/dndupload.js @@ -113,6 +113,13 @@ M.course_dndupload = { self.sectionRefreshed.bind(self) ); }); + document.addEventListener('scroll', function() { + sections.each(function(el) { + if (el.hasClass('dndupload-dropzone')) { + self.rePositionPreviewInfoElement(el); + } + }, this); + }, true); }, /** @@ -295,6 +302,24 @@ M.course_dndupload = { return false; }, + /** + * Check if the event includes only data of the Files type + * + * 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. + * + * @param {Event} e the event details + * @return true if the data types contains only Files related type + */ + typesIncludesFilesOnly: function(e) { + return e._event.dataTransfer.types.every(function(currentType) { + return (currentType.toLowerCase() != 'text/uri-list' + && currentType.toLowerCase() != 'text/html' + && currentType.toLowerCase() != 'text/plain' + ); + }); + }, + /** * Look through the event data, checking it against the registered data types * (in order of priority) and return details of the first matching data type @@ -319,7 +344,7 @@ M.course_dndupload = { } // Check for files first. - if (this.types_includes(e, 'Files')) { + if (this.types_includes(e, 'Files') && this.typesIncludesFilesOnly(e)) { if (e.type != 'drop' || e._event.dataTransfer.files.length != 0) { if (this.handlers.filehandlers.length == 0) { return false; // No available file handlers - ignore this drag. @@ -397,7 +422,7 @@ M.course_dndupload = { } } - this.show_preview_element(section, type); + this.showPreviewInfoElement(section); return false; }, @@ -549,8 +574,9 @@ M.course_dndupload = { * Hide any visible dndupload-preview elements on the page */ hide_preview_element: function() { - this.Y.all('li.dndupload-preview').addClass('dndupload-hidden'); + this.Y.all('.dndupload-preview-wrapper').addClass('dndupload-hidden'); this.Y.all('.dndupload-over').removeClass('dndupload-over'); + this.Y.all('.dndupload-dropzone').removeClass('dndupload-dropzone'); }, /** @@ -558,6 +584,7 @@ M.course_dndupload = { * the correct message * @param section the YUI node representing the selected course section * @param type the details of the data type detected in the drag (including the message to display) + * @deprecated Since Moodle 4.0. Please use showPreviewInfoElement() instead. */ show_preview_element: function(section, type) { this.hide_preview_element(); @@ -569,37 +596,90 @@ M.course_dndupload = { node.firstChild.nodeValue = type.addmessage; }, + /** + * Unhide the preview information element for the given section and set it to display + * the correct message + * @param {Object} section the YUI node representing the selected course section + */ + showPreviewInfoElement: function(section) { + this.hide_preview_element(); + section.one('.dndupload-preview-wrapper').removeClass('dndupload-hidden'); + section.addClass('dndupload-dropzone'); + this.rePositionPreviewInfoElement(section); + }, + /** * Add the preview element to a course section. Note: this needs to be done before 'addEventListener' * is called, otherwise Firefox will ignore events generated when the mouse is over the preview * element (instead of passing them up to the parent element) - * @param section the YUI node representing the selected course section + * @param {Object} section the YUI node representing the selected course section */ add_preview_element: function(section) { - var modsel = this.get_mods_element(section); - var preview = { - li: document.createElement('li'), + const modsEl = this.get_mods_element(section); + + // Create overlay div. + const overlay = document.createElement('div'); + overlay.className = 'dndupload-preview-overlay'; + + modsEl.get('parentNode').appendChild(overlay); + + // Create preview div. + const preview = { + wrapper: document.createElement('div'), div: document.createElement('div'), - icon: document.createElement('img'), - namespan: document.createElement('span') + iconSpan: document.createElement('span'), + nameSpan: document.createElement('span') }; - preview.li.className = 'dndupload-preview dndupload-hidden'; - - preview.div.className = 'mod-indent'; - preview.li.appendChild(preview.div); - - preview.icon.src = M.util.image_url('t/addfile'); - preview.icon.className = 'icon'; - preview.div.appendChild(preview.icon); - + preview.wrapper.className = 'dndupload-preview-wrapper dndupload-hidden'; + preview.wrapper.appendChild(preview.div); + preview.div.className = 'dndupload-preview'; preview.div.appendChild(document.createTextNode(' ')); + preview.iconSpan.className = 'fa fa-arrow-circle-o-down'; + preview.nameSpan.className = 'instancename'; + preview.nameSpan.innerHTML = M.util.get_string('addfilehere', 'moodle'); + preview.div.appendChild(preview.iconSpan); + preview.div.appendChild(preview.nameSpan); - preview.namespan.className = 'instancename'; - preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle'); - preview.div.appendChild(preview.namespan); + modsEl.get('parentNode').appendChild(preview.wrapper); + }, - modsel.appendChild(preview.li); + /** + * Re-position the preview information element by calculating the section position. + * + * @param {Object} section the YUI node representing the selected course section + */ + rePositionPreviewInfoElement: function(section) { + const sectionElement = document.getElementById(section.get('id')); + const rect = sectionElement.getBoundingClientRect(); + const sectionHeight = parseInt(window.getComputedStyle(sectionElement).height, 10); + const sectionOffset = rect.top; + const preview = sectionElement.querySelector('.dndupload-preview-wrapper'); + const previewHeight = parseInt(window.getComputedStyle(preview).height, 10) + + (2 * parseInt(window.getComputedStyle(preview).padding, 10)); + 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('nav.navbar.fixed-top'); + 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'; + } + + preview.style.top = top + preview.style.bottom = bottom; }, /** diff --git a/lang/en/moodle.php b/lang/en/moodle.php index fc09577e834..4b9536e2978 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -58,7 +58,7 @@ $string['added'] = 'Added {$a}'; $string['addedtogroup'] = 'Added to group "{$a}"'; $string['addedtogroupnot'] = 'Not added to group "{$a}"'; $string['addedtogroupnotenrolled'] = 'Not added to group "{$a}", because not enrolled in course'; -$string['addfilehere'] = 'Add file(s) here'; +$string['addfilehere'] = 'Drop files here to add them at the bottom of this section'; $string['addinganew'] = 'Adding a new {$a}'; $string['additionalcustomnav'] = 'Additional custom navigation'; $string['addnew'] = 'Add a new {$a}'; diff --git a/lib/amd/build/local/reactive/dragdrop.min.js b/lib/amd/build/local/reactive/dragdrop.min.js index 87ce57a254d..a79f946e554 100644 --- a/lib/amd/build/local/reactive/dragdrop.min.js +++ b/lib/amd/build/local/reactive/dragdrop.min.js @@ -62,6 +62,6 @@ define("core/local/reactive/dragdrop",["exports","core/local/reactive/basecompon * @class core/local/reactive/dragdrop * @copyright 2021 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_basecomponent=(obj=_basecomponent)&&obj.__esModule?obj:{default:obj};let activeDropData=new Map,dragStartPoint={};class _default extends _basecomponent.default{create(parent){var _parent$name,_parent$classes,_this$parent$draggabl,_this$parent$relative;this.name="".concat(null!==(_parent$name=parent.name)&&void 0!==_parent$name?_parent$name:"unkown","_dragdrop"),this.classes=Object.assign({BODYDRAGGING:"dragging",DRAGGABLEREADY:"draggable",DROPREADY:"dropready",DRAGOVER:"dragover",DRAGGING:"dragging",DROPUP:"drop-up",DROPDOWN:"drop-down",DROPZONE:"drop-zone",DRAGICON:"dragicon"},null!==(_parent$classes=null==parent?void 0:parent.classes)&&void 0!==_parent$classes?_parent$classes:{}),this.fullregion=parent.fullregion,this.parent=parent,this.autoconfigDraggable=null===(_this$parent$draggabl=this.parent.draggable)||void 0===_this$parent$draggabl||_this$parent$draggabl,this.relativeDrag=null!==(_this$parent$relative=this.parent.relativeDrag)&&void 0!==_this$parent$relative&&_this$parent$relative,this.entercount=0,this.dropzonevisible=!1}getClasses(){return this.classes}stateReady(){"function"==typeof this.parent.validateDropData&&(this.element.classList.add(this.classes.DROPREADY),this.addEventListener(this.element,"dragenter",this._dragEnter),this.addEventListener(this.element,"dragleave",this._dragLeave),this.addEventListener(this.element,"dragover",this._dragOver),this.addEventListener(this.element,"drop",this._drop)),this.autoconfigDraggable&&"function"==typeof this.parent.getDraggableData&&this.setDraggable(!0)}setDraggable(value){if("function"!=typeof this.parent.getDraggableData)throw new Error("Draggable components must have a getDraggableData method");this.element.setAttribute("draggable",value),value?(this.addEventListener(this.element,"dragstart",this._dragStart),this.addEventListener(this.element,"dragend",this._dragEnd),this.element.classList.add(this.classes.DRAGGABLEREADY)):(this.removeEventListener(this.element,"dragstart",this._dragStart),this.removeEventListener(this.element,"dragend",this._dragEnd),this.element.classList.remove(this.classes.DRAGGABLEREADY))}_dragStart(event){var _this$fullregion;if(document.activeElement.matches("textarea, input"))return void event.preventDefault();const dropdata=this.parent.getDraggableData();if(!dropdata)return;dragStartPoint={pageX:event.pageX,pageY:event.pageY},event.stopPropagation(),activeDropData.set(this.reactive,dropdata),document.body.classList.add(this.classes.BODYDRAGGING),this.element.classList.add(this.classes.DRAGGING),null===(_this$fullregion=this.fullregion)||void 0===_this$fullregion||_this$fullregion.classList.add(this.classes.DRAGGING);let dragImage=this.element;if(void 0!==this.parent.setDragImage){const customImage=this.parent.setDragImage(dropdata,event);customImage&&(dragImage=customImage)}const position={x:0,y:0};this.relativeDrag&&(position.x=event.offsetX,position.y=event.offsetY),event.dataTransfer.setDragImage(dragImage,position.x,position.y),this._callParentMethod("dragStart",dropdata,event)}_dragEnd(event){var _this$fullregion2;const dropdata=activeDropData.get(this.reactive);dropdata&&(activeDropData.delete(this.reactive),document.body.classList.remove(this.classes.BODYDRAGGING),this.element.classList.remove(this.classes.DRAGGING),null===(_this$fullregion2=this.fullregion)||void 0===_this$fullregion2||_this$fullregion2.classList.remove(this.classes.DRAGGING),this._addEventTotalMovement(event),this._callParentMethod("dragEnd",dropdata,event))}_dragEnter(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount++,this.element.classList.add(this.classes.DRAGOVER),1!=this.entercount||this.dropzonevisible||(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event)))}_dragOver(event){const dropdata=this._processEvent(event);dropdata&&!this.dropzonevisible&&(this.dropzonevisible=!0,this.element.classList.add(this.classes.DRAGOVER),this._callParentMethod("showDropZone",dropdata,event))}_dragLeave(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount--,0==this.entercount&&this.dropzonevisible&&(this.dropzonevisible=!1,this.element.classList.remove(this.classes.DRAGOVER),this._callParentMethod("hideDropZone",dropdata,event)))}_drop(event){const dropdata=this._processEvent(event);dropdata&&(this.entercount=0,this.dropzonevisible&&(this.dropzonevisible=!1,this._callParentMethod("hideDropZone",dropdata,event)),this.element.classList.remove(this.classes.DRAGOVER),this._callParentMethod("drop",dropdata,event),dragStartPoint={})}_processEvent(event){const dropdata=this._getDropData(event);return dropdata&&this.parent.validateDropData(dropdata)?(event.preventDefault(),event.stopPropagation(),this._addEventTotalMovement(event),dropdata):null}_addEventTotalMovement(event){if(void 0===dragStartPoint.pageX||void 0===event.pageX)return;event.fixedMovementX=event.pageX-dragStartPoint.pageX,event.fixedMovementY=event.pageY-dragStartPoint.pageY,event.initialPageX=dragStartPoint.pageX,event.initialPageY=dragStartPoint.pageY;const current=this.element.getBoundingClientRect();if(event.newFixedTop=current.top+event.fixedMovementY,event.newFixedLeft=current.left+event.fixedMovementX,void 0!==this.fullregion){const current=this.fullregion.getBoundingClientRect();event.newRegionFixedxTop=current.top+event.fixedMovementY,event.newRegionFixedxLeft=current.left+event.fixedMovementX}}_callParentMethod(methodname,dropdata,event){"function"==typeof this.parent[methodname]&&this.parent[methodname](dropdata,event)}_getDropData(event){if(!this._containsFiles(event))return activeDropData.get(this.reactive)}_containsFiles(event){if(event.dataTransfer.types)for(var i=0;i0)&&event.dataTransfer.types.every((type=>"Files"===type))}}return _exports.default=_default,_exports.default})); //# sourceMappingURL=dragdrop.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/reactive/dragdrop.min.js.map b/lib/amd/build/local/reactive/dragdrop.min.js.map index 8bf56984ddf..fbb6f7f8414 100644 --- a/lib/amd/build/local/reactive/dragdrop.min.js.map +++ b/lib/amd/build/local/reactive/dragdrop.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dragdrop.min.js","sources":["../../../src/local/reactive/dragdrop.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Drag and drop helper component.\n *\n * This component is used to delegate drag and drop handling.\n *\n * To delegate the logic to this particular element the component should create a new instance\n * passing \"this\" as param. The component will use all the necessary callbacks and add all the\n * necessary listeners to the component element.\n *\n * Component attributes used by dragdrop module:\n * - element: the draggable or dropzone element.\n * - (optional) classes: object with alternative CSS classes\n * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if\n * the draggable element affects a bigger region (for example a draggable\n * title).\n * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a\n * getDraggableData method. If this value is false draggable\n * property must be defined using setDraggable method.\n * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the\n * mouse position to prevent the mouse from covering it. If this attribute\n * is true the drag image will be located at the click offset.\n *\n * Methods the parent component should have for making it draggable:\n *\n * - getDraggableData(): Object|data\n * Return the data that will be passed to any valid dropzone while it is dragged.\n * If the component has this method, the dragdrop module will enable the dragging,\n * this is the only required method for dragging.\n * If at the dragging moment this method returns a false|null|undefined, the dragging\n * actions won't be captured.\n *\n * - (optional) dragStart(Object dropdata, Event event): void\n * - (optional) dragEnd(Object dropdata, Event event): void\n * Callbacks dragdrop will call when the element is dragged and getDraggableData\n * return some data.\n *\n * Methods the parent component should have for enabling it as a dropzone:\n *\n * - validateDropData(Object dropdata): boolean\n * If that method exists, the dragdrop module will automathically configure the element as dropzone.\n * This method will return true if the dropdata is accepted. In case it returns false, no drag and\n * drop event will be listened for this specific dragged dropdata.\n *\n * - (Optional) showDropZone(Object dropdata, Event event): void\n * - (Optional) hideDropZone(Object dropdata, Event event): void\n * Methods called when a valid dragged data pass over the element.\n *\n * - (Optional) drop(Object dropdata, Event event): void\n * Called when a valid dragged element is dropped over the element.\n *\n * Note that none of this methods will be called if validateDropData\n * returns a false value.\n *\n * This module will also add or remove several CSS classes from both dragged elements and dropzones.\n * See the \"this.classes\" in the create method for more details. In case the parent component wants\n * to use the same classes, it can use the getClasses method. On the other hand, if the parent\n * component has an alternative \"classes\" attribute, this will override the default drag and drop\n * classes.\n *\n * @module core/local/reactive/dragdrop\n * @class core/local/reactive/dragdrop\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\n\n// Map with the dragged element generate by an specific reactive applications.\n// Potentially, any component can generate a draggable element to interact with other\n// page elements. However, the dragged data is specific and could only interact with\n// components of the same reactive instance.\nlet activeDropData = new Map();\n\n// Drag & Drop API provides the final drop point and incremental movements but we can\n// provide also starting points and displacements. Absolute displacements simplifies\n// moving components with aboslute position around the page.\nlet dragStartPoint = {};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {BaseComponent} parent the parent component.\n */\n create(parent) {\n // Optional component name for debugging.\n this.name = `${parent.name ?? 'unkown'}_dragdrop`;\n\n // Default drag and drop classes.\n this.classes = Object.assign(\n {\n // This class indicate a dragging action is active at a page level.\n BODYDRAGGING: 'dragging',\n\n // Added when draggable and drop are ready.\n DRAGGABLEREADY: 'draggable',\n DROPREADY: 'dropready',\n\n // When a valid drag element is over the element.\n DRAGOVER: 'dragover',\n // When a the component is dragged.\n DRAGGING: 'dragging',\n\n // Dropzones classes names.\n DROPUP: 'drop-up',\n DROPDOWN: 'drop-down',\n DROPZONE: 'drop-zone',\n\n // Drag icon class.\n DRAGICON: 'dragicon',\n },\n parent?.classes ?? {}\n );\n\n // Add the affected region if any.\n this.fullregion = parent.fullregion;\n\n // Keep parent to execute drap and drop handlers.\n this.parent = parent;\n\n // Check if parent handle draggable manually.\n this.autoconfigDraggable = this.parent.draggable ?? true;\n\n // Drag image relative position.\n this.relativeDrag = this.parent.relativeDrag ?? false;\n\n // Sub HTML elements will trigger extra dragEnter and dragOver all the time.\n // To prevent that from affecting dropzones, we need to count the enters and leaves.\n this.entercount = 0;\n\n // Stores if the droparea is shown or not.\n this.dropzonevisible = false;\n\n }\n\n /**\n * Return the component drag and drop CSS classes.\n *\n * @returns {Object} the dragdrop css classes\n */\n getClasses() {\n return this.classes;\n }\n\n /**\n * Initial state ready method.\n *\n * This method will add all the necessary event listeners to the component depending on the\n * parent methods.\n * - Add drop events to the element if the parent component has validateDropData method.\n * - Configure the elements draggable if the parent component has getDraggableData method.\n */\n stateReady() {\n // Add drop events to the element if the parent component has dropable types.\n if (typeof this.parent.validateDropData === 'function') {\n this.element.classList.add(this.classes.DROPREADY);\n this.addEventListener(this.element, 'dragenter', this._dragEnter);\n this.addEventListener(this.element, 'dragleave', this._dragLeave);\n this.addEventListener(this.element, 'dragover', this._dragOver);\n this.addEventListener(this.element, 'drop', this._drop);\n }\n\n // Configure the elements draggable if the parent component has dragable data.\n if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {\n this.setDraggable(true);\n }\n }\n\n /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (typeof this.parent.getDraggableData !== 'function') {\n throw new Error(`Draggable components must have a getDraggableData method`);\n }\n this.element.setAttribute('draggable', value);\n if (value) {\n this.addEventListener(this.element, 'dragstart', this._dragStart);\n this.addEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.add(this.classes.DRAGGABLEREADY);\n } else {\n this.removeEventListener(this.element, 'dragstart', this._dragStart);\n this.removeEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.remove(this.classes.DRAGGABLEREADY);\n }\n }\n\n /**\n * Drag start event handler.\n *\n * This method will generate the current dropable data. This data is the one used to determine\n * if a droparea accepts the dropping or not.\n *\n * @param {Event} event the event.\n */\n _dragStart(event) {\n // Cancel dragging if any editable form element is focussed.\n if (document.activeElement.matches(`textarea, input`)) {\n event.preventDefault();\n return;\n }\n\n const dropdata = this.parent.getDraggableData();\n if (!dropdata) {\n return;\n }\n\n // Save the starting point.\n dragStartPoint = {\n pageX: event.pageX,\n pageY: event.pageY,\n };\n\n // If the drag event is accepted we prevent any other draggable element from interfiering.\n event.stopPropagation();\n\n // Save the drop data of the current reactive intance.\n activeDropData.set(this.reactive, dropdata);\n\n // Add some CSS classes to indicate the state.\n document.body.classList.add(this.classes.BODYDRAGGING);\n this.element.classList.add(this.classes.DRAGGING);\n this.fullregion?.classList.add(this.classes.DRAGGING);\n\n // Force the drag image. This makes the UX more consistent in case the\n // user dragged an internal element like a link or some other element.\n let dragImage = this.element;\n if (this.parent.setDragImage !== undefined) {\n const customImage = this.parent.setDragImage(dropdata, event);\n if (customImage) {\n dragImage = customImage;\n }\n }\n // Define the image position relative to the mouse.\n const position = {x: 0, y: 0};\n if (this.relativeDrag) {\n position.x = event.offsetX;\n position.y = event.offsetY;\n }\n event.dataTransfer.setDragImage(dragImage, position.x, position.y);\n\n this._callParentMethod('dragStart', dropdata, event);\n }\n\n /**\n * Drag end event handler.\n *\n * @param {Event} event the event.\n */\n _dragEnd(event) {\n const dropdata = activeDropData.get(this.reactive);\n if (!dropdata) {\n return;\n }\n\n // Remove the current dropdata.\n activeDropData.delete(this.reactive);\n\n // Remove the dragging classes.\n document.body.classList.remove(this.classes.BODYDRAGGING);\n this.element.classList.remove(this.classes.DRAGGING);\n this.fullregion?.classList.remove(this.classes.DRAGGING);\n\n // We add the total movement to the event in case the component\n // wants to move its absolute position.\n this._addEventTotalMovement(event);\n\n this._callParentMethod('dragEnd', dropdata, event);\n }\n\n /**\n * Drag enter event handler.\n *\n * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragEnter(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount++;\n this.element.classList.add(this.classes.DRAGOVER);\n if (this.entercount == 1 && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drag over event handler.\n *\n * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases\n * the API won't trigger any dragEnter because the dragged alement was already there. We use the\n * dropzonevisible to determine if the component needs to display the dropzones or not.\n *\n * @param {Event} event the event.\n */\n _dragOver(event) {\n const dropdata = this._processEvent(event);\n if (dropdata && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n\n /**\n * Drag over leave handler.\n *\n * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragLeave(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount--;\n if (this.entercount == 0 && this.dropzonevisible) {\n this.dropzonevisible = false;\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drop event handler.\n *\n * This method will call both hideDropZones and drop methods on the parent component.\n *\n * @param {Event} event the event.\n */\n _drop(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount = 0;\n if (this.dropzonevisible) {\n this.dropzonevisible = false;\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('drop', dropdata, event);\n // An accepted drop resets the initial position.\n // Save the starting point.\n dragStartPoint = {};\n }\n }\n\n /**\n * Process a drag and drop event and delegate logic to the parent component.\n *\n * @param {Event} event the drag and drop event\n * @return {Object|false} the dropdata or null if the event should not be processed\n */\n _processEvent(event) {\n const dropdata = this._getDropData(event);\n if (!dropdata) {\n return null;\n }\n if (this.parent.validateDropData(dropdata)) {\n // All accepted drag&drop event must prevent bubbling and defaults, otherwise\n // parent dragdrop instances could capture it by mistake.\n event.preventDefault();\n event.stopPropagation();\n this._addEventTotalMovement(event);\n return dropdata;\n }\n return null;\n }\n\n /**\n * Add the total amout of movement to a mouse event.\n *\n * @param {MouseEvent} event\n */\n _addEventTotalMovement(event) {\n if (dragStartPoint.pageX === undefined || event.pageX === undefined) {\n return;\n }\n event.fixedMovementX = event.pageX - dragStartPoint.pageX;\n event.fixedMovementY = event.pageY - dragStartPoint.pageY;\n event.initialPageX = dragStartPoint.pageX;\n event.initialPageY = dragStartPoint.pageY;\n // The element possible new top.\n const current = this.element.getBoundingClientRect();\n // Add the new position fixed position.\n event.newFixedTop = current.top + event.fixedMovementY;\n event.newFixedLeft = current.left + event.fixedMovementX;\n // The affected region possible new top.\n if (this.fullregion !== undefined) {\n const current = this.fullregion.getBoundingClientRect();\n event.newRegionFixedxTop = current.top + event.fixedMovementY;\n event.newRegionFixedxLeft = current.left + event.fixedMovementX;\n }\n }\n\n /**\n * Convenient method for calling parent component functions if present.\n *\n * @param {string} methodname the name of the method\n * @param {Object} dropdata the current drop data object\n * @param {Event} event the original event\n */\n _callParentMethod(methodname, dropdata, event) {\n if (typeof this.parent[methodname] === 'function') {\n this.parent[methodname](dropdata, event);\n }\n }\n\n /**\n * Get the current dropdata for a specific event.\n *\n * The browser can generate drag&drop events related to several user interactions:\n * - Drag a page elements: this case is registered in the activeDropData map\n * - Drag some HTML selections: ignored for now\n * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.\n *\n * @param {Event} event the original event.\n * @returns {Object|undefined} with the dragged data (or undefined if none)\n */\n _getDropData(event) {\n if (this._containsFiles(event)) {\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains files.\n *\n * Files dragging does not generate drop data because they came from outsite the page and the component\n * must check it before validating the event.\n *\n * @param {Event} event the original event.\n * @returns {boolean} if the drag dataTransfers contains files.\n */\n _containsFiles(event) {\n if (event.dataTransfer.types) {\n for (var i = 0; i < event.dataTransfer.types.length; i++) {\n if (event.dataTransfer.types[i] == \"Files\") {\n return true;\n }\n }\n }\n return false;\n }\n}\n"],"names":["activeDropData","Map","dragStartPoint","BaseComponent","create","parent","name","classes","Object","assign","BODYDRAGGING","DRAGGABLEREADY","DROPREADY","DRAGOVER","DRAGGING","DROPUP","DROPDOWN","DROPZONE","DRAGICON","fullregion","autoconfigDraggable","this","draggable","relativeDrag","entercount","dropzonevisible","getClasses","stateReady","validateDropData","element","classList","add","addEventListener","_dragEnter","_dragLeave","_dragOver","_drop","getDraggableData","setDraggable","value","Error","setAttribute","_dragStart","_dragEnd","removeEventListener","remove","event","document","activeElement","matches","preventDefault","dropdata","pageX","pageY","stopPropagation","set","reactive","body","dragImage","undefined","setDragImage","customImage","position","x","y","offsetX","offsetY","dataTransfer","_callParentMethod","get","delete","_addEventTotalMovement","_processEvent","_getDropData","fixedMovementX","fixedMovementY","initialPageX","initialPageY","current","getBoundingClientRect","newFixedTop","top","newFixedLeft","left","newRegionFixedxTop","newRegionFixedxLeft","methodname","_containsFiles","types","i","length"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"dragdrop.min.js","sources":["../../../src/local/reactive/dragdrop.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Drag and drop helper component.\n *\n * This component is used to delegate drag and drop handling.\n *\n * To delegate the logic to this particular element the component should create a new instance\n * passing \"this\" as param. The component will use all the necessary callbacks and add all the\n * necessary listeners to the component element.\n *\n * Component attributes used by dragdrop module:\n * - element: the draggable or dropzone element.\n * - (optional) classes: object with alternative CSS classes\n * - (optional) fullregion: page element affeted by the elementy dragging. Use this attribute if\n * the draggable element affects a bigger region (for example a draggable\n * title).\n * - (optional) autoconfigDraggable: by default, the component will be draggable if it has a\n * getDraggableData method. If this value is false draggable\n * property must be defined using setDraggable method.\n * - (optional) relativeDrag: by default the drag image is located at point (0,0) relative to the\n * mouse position to prevent the mouse from covering it. If this attribute\n * is true the drag image will be located at the click offset.\n *\n * Methods the parent component should have for making it draggable:\n *\n * - getDraggableData(): Object|data\n * Return the data that will be passed to any valid dropzone while it is dragged.\n * If the component has this method, the dragdrop module will enable the dragging,\n * this is the only required method for dragging.\n * If at the dragging moment this method returns a false|null|undefined, the dragging\n * actions won't be captured.\n *\n * - (optional) dragStart(Object dropdata, Event event): void\n * - (optional) dragEnd(Object dropdata, Event event): void\n * Callbacks dragdrop will call when the element is dragged and getDraggableData\n * return some data.\n *\n * Methods the parent component should have for enabling it as a dropzone:\n *\n * - validateDropData(Object dropdata): boolean\n * If that method exists, the dragdrop module will automathically configure the element as dropzone.\n * This method will return true if the dropdata is accepted. In case it returns false, no drag and\n * drop event will be listened for this specific dragged dropdata.\n *\n * - (Optional) showDropZone(Object dropdata, Event event): void\n * - (Optional) hideDropZone(Object dropdata, Event event): void\n * Methods called when a valid dragged data pass over the element.\n *\n * - (Optional) drop(Object dropdata, Event event): void\n * Called when a valid dragged element is dropped over the element.\n *\n * Note that none of this methods will be called if validateDropData\n * returns a false value.\n *\n * This module will also add or remove several CSS classes from both dragged elements and dropzones.\n * See the \"this.classes\" in the create method for more details. In case the parent component wants\n * to use the same classes, it can use the getClasses method. On the other hand, if the parent\n * component has an alternative \"classes\" attribute, this will override the default drag and drop\n * classes.\n *\n * @module core/local/reactive/dragdrop\n * @class core/local/reactive/dragdrop\n * @copyright 2021 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport BaseComponent from 'core/local/reactive/basecomponent';\n\n// Map with the dragged element generate by an specific reactive applications.\n// Potentially, any component can generate a draggable element to interact with other\n// page elements. However, the dragged data is specific and could only interact with\n// components of the same reactive instance.\nlet activeDropData = new Map();\n\n// Drag & Drop API provides the final drop point and incremental movements but we can\n// provide also starting points and displacements. Absolute displacements simplifies\n// moving components with aboslute position around the page.\nlet dragStartPoint = {};\n\nexport default class extends BaseComponent {\n\n /**\n * Constructor hook.\n *\n * @param {BaseComponent} parent the parent component.\n */\n create(parent) {\n // Optional component name for debugging.\n this.name = `${parent.name ?? 'unkown'}_dragdrop`;\n\n // Default drag and drop classes.\n this.classes = Object.assign(\n {\n // This class indicate a dragging action is active at a page level.\n BODYDRAGGING: 'dragging',\n\n // Added when draggable and drop are ready.\n DRAGGABLEREADY: 'draggable',\n DROPREADY: 'dropready',\n\n // When a valid drag element is over the element.\n DRAGOVER: 'dragover',\n // When a the component is dragged.\n DRAGGING: 'dragging',\n\n // Dropzones classes names.\n DROPUP: 'drop-up',\n DROPDOWN: 'drop-down',\n DROPZONE: 'drop-zone',\n\n // Drag icon class.\n DRAGICON: 'dragicon',\n },\n parent?.classes ?? {}\n );\n\n // Add the affected region if any.\n this.fullregion = parent.fullregion;\n\n // Keep parent to execute drap and drop handlers.\n this.parent = parent;\n\n // Check if parent handle draggable manually.\n this.autoconfigDraggable = this.parent.draggable ?? true;\n\n // Drag image relative position.\n this.relativeDrag = this.parent.relativeDrag ?? false;\n\n // Sub HTML elements will trigger extra dragEnter and dragOver all the time.\n // To prevent that from affecting dropzones, we need to count the enters and leaves.\n this.entercount = 0;\n\n // Stores if the droparea is shown or not.\n this.dropzonevisible = false;\n\n }\n\n /**\n * Return the component drag and drop CSS classes.\n *\n * @returns {Object} the dragdrop css classes\n */\n getClasses() {\n return this.classes;\n }\n\n /**\n * Initial state ready method.\n *\n * This method will add all the necessary event listeners to the component depending on the\n * parent methods.\n * - Add drop events to the element if the parent component has validateDropData method.\n * - Configure the elements draggable if the parent component has getDraggableData method.\n */\n stateReady() {\n // Add drop events to the element if the parent component has dropable types.\n if (typeof this.parent.validateDropData === 'function') {\n this.element.classList.add(this.classes.DROPREADY);\n this.addEventListener(this.element, 'dragenter', this._dragEnter);\n this.addEventListener(this.element, 'dragleave', this._dragLeave);\n this.addEventListener(this.element, 'dragover', this._dragOver);\n this.addEventListener(this.element, 'drop', this._drop);\n }\n\n // Configure the elements draggable if the parent component has dragable data.\n if (this.autoconfigDraggable && typeof this.parent.getDraggableData === 'function') {\n this.setDraggable(true);\n }\n }\n\n /**\n * Enable or disable the draggable property.\n *\n * @param {bool} value the new draggable value\n */\n setDraggable(value) {\n if (typeof this.parent.getDraggableData !== 'function') {\n throw new Error(`Draggable components must have a getDraggableData method`);\n }\n this.element.setAttribute('draggable', value);\n if (value) {\n this.addEventListener(this.element, 'dragstart', this._dragStart);\n this.addEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.add(this.classes.DRAGGABLEREADY);\n } else {\n this.removeEventListener(this.element, 'dragstart', this._dragStart);\n this.removeEventListener(this.element, 'dragend', this._dragEnd);\n this.element.classList.remove(this.classes.DRAGGABLEREADY);\n }\n }\n\n /**\n * Drag start event handler.\n *\n * This method will generate the current dropable data. This data is the one used to determine\n * if a droparea accepts the dropping or not.\n *\n * @param {Event} event the event.\n */\n _dragStart(event) {\n // Cancel dragging if any editable form element is focussed.\n if (document.activeElement.matches(`textarea, input`)) {\n event.preventDefault();\n return;\n }\n\n const dropdata = this.parent.getDraggableData();\n if (!dropdata) {\n return;\n }\n\n // Save the starting point.\n dragStartPoint = {\n pageX: event.pageX,\n pageY: event.pageY,\n };\n\n // If the drag event is accepted we prevent any other draggable element from interfiering.\n event.stopPropagation();\n\n // Save the drop data of the current reactive intance.\n activeDropData.set(this.reactive, dropdata);\n\n // Add some CSS classes to indicate the state.\n document.body.classList.add(this.classes.BODYDRAGGING);\n this.element.classList.add(this.classes.DRAGGING);\n this.fullregion?.classList.add(this.classes.DRAGGING);\n\n // Force the drag image. This makes the UX more consistent in case the\n // user dragged an internal element like a link or some other element.\n let dragImage = this.element;\n if (this.parent.setDragImage !== undefined) {\n const customImage = this.parent.setDragImage(dropdata, event);\n if (customImage) {\n dragImage = customImage;\n }\n }\n // Define the image position relative to the mouse.\n const position = {x: 0, y: 0};\n if (this.relativeDrag) {\n position.x = event.offsetX;\n position.y = event.offsetY;\n }\n event.dataTransfer.setDragImage(dragImage, position.x, position.y);\n\n this._callParentMethod('dragStart', dropdata, event);\n }\n\n /**\n * Drag end event handler.\n *\n * @param {Event} event the event.\n */\n _dragEnd(event) {\n const dropdata = activeDropData.get(this.reactive);\n if (!dropdata) {\n return;\n }\n\n // Remove the current dropdata.\n activeDropData.delete(this.reactive);\n\n // Remove the dragging classes.\n document.body.classList.remove(this.classes.BODYDRAGGING);\n this.element.classList.remove(this.classes.DRAGGING);\n this.fullregion?.classList.remove(this.classes.DRAGGING);\n\n // We add the total movement to the event in case the component\n // wants to move its absolute position.\n this._addEventTotalMovement(event);\n\n this._callParentMethod('dragEnd', dropdata, event);\n }\n\n /**\n * Drag enter event handler.\n *\n * The JS drag&drop API triggers several dragenter events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragEnter(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount++;\n this.element.classList.add(this.classes.DRAGOVER);\n if (this.entercount == 1 && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drag over event handler.\n *\n * We only use dragover event when a draggable action starts inside a valid dropzone. In those cases\n * the API won't trigger any dragEnter because the dragged alement was already there. We use the\n * dropzonevisible to determine if the component needs to display the dropzones or not.\n *\n * @param {Event} event the event.\n */\n _dragOver(event) {\n const dropdata = this._processEvent(event);\n if (dropdata && !this.dropzonevisible) {\n this.dropzonevisible = true;\n this.element.classList.add(this.classes.DRAGOVER);\n this._callParentMethod('showDropZone', dropdata, event);\n }\n }\n\n /**\n * Drag over leave handler.\n *\n * The JS drag&drop API triggers several dragleave events on the same element because it bubbles the\n * child events as well. To prevent this form affecting the dropzones display, this methods use\n * \"entercount\" to determine if it's one extra child event or a valid one.\n *\n * @param {Event} event the event.\n */\n _dragLeave(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount--;\n if (this.entercount == 0 && this.dropzonevisible) {\n this.dropzonevisible = false;\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n }\n }\n\n /**\n * Drop event handler.\n *\n * This method will call both hideDropZones and drop methods on the parent component.\n *\n * @param {Event} event the event.\n */\n _drop(event) {\n const dropdata = this._processEvent(event);\n if (dropdata) {\n this.entercount = 0;\n if (this.dropzonevisible) {\n this.dropzonevisible = false;\n this._callParentMethod('hideDropZone', dropdata, event);\n }\n this.element.classList.remove(this.classes.DRAGOVER);\n this._callParentMethod('drop', dropdata, event);\n // An accepted drop resets the initial position.\n // Save the starting point.\n dragStartPoint = {};\n }\n }\n\n /**\n * Process a drag and drop event and delegate logic to the parent component.\n *\n * @param {Event} event the drag and drop event\n * @return {Object|false} the dropdata or null if the event should not be processed\n */\n _processEvent(event) {\n const dropdata = this._getDropData(event);\n if (!dropdata) {\n return null;\n }\n if (this.parent.validateDropData(dropdata)) {\n // All accepted drag&drop event must prevent bubbling and defaults, otherwise\n // parent dragdrop instances could capture it by mistake.\n event.preventDefault();\n event.stopPropagation();\n this._addEventTotalMovement(event);\n return dropdata;\n }\n return null;\n }\n\n /**\n * Add the total amout of movement to a mouse event.\n *\n * @param {MouseEvent} event\n */\n _addEventTotalMovement(event) {\n if (dragStartPoint.pageX === undefined || event.pageX === undefined) {\n return;\n }\n event.fixedMovementX = event.pageX - dragStartPoint.pageX;\n event.fixedMovementY = event.pageY - dragStartPoint.pageY;\n event.initialPageX = dragStartPoint.pageX;\n event.initialPageY = dragStartPoint.pageY;\n // The element possible new top.\n const current = this.element.getBoundingClientRect();\n // Add the new position fixed position.\n event.newFixedTop = current.top + event.fixedMovementY;\n event.newFixedLeft = current.left + event.fixedMovementX;\n // The affected region possible new top.\n if (this.fullregion !== undefined) {\n const current = this.fullregion.getBoundingClientRect();\n event.newRegionFixedxTop = current.top + event.fixedMovementY;\n event.newRegionFixedxLeft = current.left + event.fixedMovementX;\n }\n }\n\n /**\n * Convenient method for calling parent component functions if present.\n *\n * @param {string} methodname the name of the method\n * @param {Object} dropdata the current drop data object\n * @param {Event} event the original event\n */\n _callParentMethod(methodname, dropdata, event) {\n if (typeof this.parent[methodname] === 'function') {\n this.parent[methodname](dropdata, event);\n }\n }\n\n /**\n * Get the current dropdata for a specific event.\n *\n * The browser can generate drag&drop events related to several user interactions:\n * - Drag a page elements: this case is registered in the activeDropData map\n * - Drag some HTML selections: ignored for now\n * - Drag a file over the browser: file drag may appear in the future but for now they are ignored.\n *\n * @param {Event} event the original event.\n * @returns {Object|undefined} with the dragged data (or undefined if none)\n */\n _getDropData(event) {\n if (this._containsOnlyFiles(event)) {\n return undefined;\n }\n return activeDropData.get(this.reactive);\n }\n\n /**\n * Check if the dragged event contains only files.\n *\n * Files dragging does not generate drop data because they came from outside the page and the component\n * must check it before validating the event.\n *\n * @param {Event} event the original event.\n * @returns {boolean} if the drag dataTransfers contains files.\n */\n _containsOnlyFiles(event) {\n if (event.dataTransfer.types && event.dataTransfer.types.length > 0) {\n // Chrome drag page images as files. To differentiate a real file from a page\n // image we need to check if all the dataTransfers types are files.\n return event.dataTransfer.types.every(type => type === 'Files');\n }\n return false;\n }\n}\n"],"names":["activeDropData","Map","dragStartPoint","BaseComponent","create","parent","name","classes","Object","assign","BODYDRAGGING","DRAGGABLEREADY","DROPREADY","DRAGOVER","DRAGGING","DROPUP","DROPDOWN","DROPZONE","DRAGICON","fullregion","autoconfigDraggable","this","draggable","relativeDrag","entercount","dropzonevisible","getClasses","stateReady","validateDropData","element","classList","add","addEventListener","_dragEnter","_dragLeave","_dragOver","_drop","getDraggableData","setDraggable","value","Error","setAttribute","_dragStart","_dragEnd","removeEventListener","remove","event","document","activeElement","matches","preventDefault","dropdata","pageX","pageY","stopPropagation","set","reactive","body","dragImage","undefined","setDragImage","customImage","position","x","y","offsetX","offsetY","dataTransfer","_callParentMethod","get","delete","_addEventTotalMovement","_processEvent","_getDropData","fixedMovementX","fixedMovementY","initialPageX","initialPageY","current","getBoundingClientRect","newFixedTop","top","newFixedLeft","left","newRegionFixedxTop","newRegionFixedxLeft","methodname","_containsOnlyFiles","types","length","every","type"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/src/local/reactive/dragdrop.js b/lib/amd/src/local/reactive/dragdrop.js index 95cb6a6f1f8..1e433242e66 100644 --- a/lib/amd/src/local/reactive/dragdrop.js +++ b/lib/amd/src/local/reactive/dragdrop.js @@ -443,28 +443,26 @@ export default class extends BaseComponent { * @returns {Object|undefined} with the dragged data (or undefined if none) */ _getDropData(event) { - if (this._containsFiles(event)) { + if (this._containsOnlyFiles(event)) { return undefined; } return activeDropData.get(this.reactive); } /** - * Check if the dragged event contains files. + * Check if the dragged event contains only files. * - * Files dragging does not generate drop data because they came from outsite the page and the component + * Files dragging does not generate drop data because they came from outside the page and the component * must check it before validating the event. * * @param {Event} event the original event. * @returns {boolean} if the drag dataTransfers contains files. */ - _containsFiles(event) { - if (event.dataTransfer.types) { - for (var i = 0; i < event.dataTransfer.types.length; i++) { - if (event.dataTransfer.types[i] == "Files") { - return true; - } - } + _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'); } return false; } diff --git a/theme/boost/scss/moodle/course.scss b/theme/boost/scss/moodle/course.scss index 5da7675bd6b..f54b82442f7 100644 --- a/theme/boost/scss/moodle/course.scss +++ b/theme/boost/scss/moodle/course.scss @@ -674,11 +674,14 @@ span.editinstructions { } .dndupload-preview { - color: #909090; - border: 1px dashed #909090; - list-style: none; - margin-top: .2em; - padding: .3em; + padding: $modal-inner-padding; + background-color: $primary; + color: $white; + text-align: center; + font-size: $font-size-lg; + max-width: 600px; + margin: 0 auto; + @include border-radius(); } .dndupload-hidden { @@ -1337,6 +1340,30 @@ $activity-add-hover: theme-color-level('primary', -10) !default; white-space: nowrap; display: inline-block; } + + &.dndupload-dropzone { + border: 2px dashed $primary; + padding-left: 2px; + padding-right: 2px; + position: relative; + + .dndupload-preview-overlay { + background-color: $white; + opacity: 0.8; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .dndupload-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; + } + } } .description .course-description-item { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b9ffcd2abda..27e391bd1d2 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -14231,11 +14231,14 @@ span.editinstructions { color: #00171b; } .dndupload-preview { - color: #909090; - border: 1px dashed #909090; - list-style: none; - margin-top: .2em; - padding: .3em; } + padding: 1rem; + background-color: #0f6cbf; + color: #fff; + text-align: center; + font-size: 1.171875rem; + max-width: 600px; + margin: 0 auto; + border-radius: 0.5rem; } .dndupload-hidden { display: none; } @@ -14717,6 +14720,24 @@ span.editinstructions { margin: 3px; white-space: nowrap; display: inline-block; } + .course-section.dndupload-dropzone { + border: 2px dashed #0f6cbf; + padding-left: 2px; + padding-right: 2px; + position: relative; } + .course-section.dndupload-dropzone .dndupload-preview-overlay { + background-color: #fff; + opacity: 0.8; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + .course-section.dndupload-dropzone .dndupload-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; } .description .course-description-item { background-color: #f8f9fa; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 0b074281110..ab0e5439a54 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -14231,11 +14231,14 @@ span.editinstructions { color: #00171b; } .dndupload-preview { - color: #909090; - border: 1px dashed #909090; - list-style: none; - margin-top: .2em; - padding: .3em; } + padding: 1rem; + background-color: #0f6cbf; + color: #fff; + text-align: center; + font-size: 1.171875rem; + max-width: 600px; + margin: 0 auto; + border-radius: 0.25rem; } .dndupload-hidden { display: none; } @@ -14717,6 +14720,24 @@ span.editinstructions { margin: 3px; white-space: nowrap; display: inline-block; } + .course-section.dndupload-dropzone { + border: 2px dashed #0f6cbf; + padding-left: 2px; + padding-right: 2px; + position: relative; } + .course-section.dndupload-dropzone .dndupload-preview-overlay { + background-color: #fff; + opacity: 0.8; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } + .course-section.dndupload-dropzone .dndupload-preview-wrapper { + position: absolute; + top: 0; + padding: 2rem; + width: 100%; } .description .course-description-item { background-color: #f8f9fa;