From b0220f27cb33965dda5ad3894ca79b07f46eb976 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 13 Feb 2024 11:26:25 +0700 Subject: [PATCH] MDL-80850 Javascript: Add new core/dropzone module --- lang/en/moodle.php | 1 + lib/amd/build/dropzone.min.js | 11 ++ lib/amd/build/dropzone.min.js.map | 1 + lib/amd/src/dropzone.js | 150 ++++++++++++++++++ .../output/icon_system_fontawesome.php | 1 + lib/templates/dropzone.mustache | 35 ++++ pix/i/cloudupload.svg | 1 + theme/boost/scss/moodle/core.scss | 42 +++++ theme/boost/style/moodle.css | 34 ++++ theme/classic/style/moodle.css | 34 ++++ 10 files changed, 310 insertions(+) create mode 100644 lib/amd/build/dropzone.min.js create mode 100644 lib/amd/build/dropzone.min.js.map create mode 100644 lib/amd/src/dropzone.js create mode 100644 lib/templates/dropzone.mustache create mode 100644 pix/i/cloudupload.svg diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 21bfc94f34d..c0c434bfb9e 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -62,6 +62,7 @@ $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'] = 'Drop files here to add them at the bottom of this section'; +$string['addfilesdrop'] = 'You can drag and drop files here to upload or click to select.'; $string['addinganew'] = 'New {$a}'; $string['additionalcustomnav'] = 'Additional custom navigation'; $string['addnew'] = 'Add a new {$a}'; diff --git a/lib/amd/build/dropzone.min.js b/lib/amd/build/dropzone.min.js new file mode 100644 index 00000000000..1024dbc7377 --- /dev/null +++ b/lib/amd/build/dropzone.min.js @@ -0,0 +1,11 @@ +define("core/dropzone",["exports","core/log","core/templates"],(function(_exports,_log,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * JavaScript to handle dropzone. + * + * @module core/dropzone + * @copyright 2024 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.4 + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_log=_interopRequireDefault(_log),_templates=_interopRequireDefault(_templates);var _default=class{constructor(dropZoneElement,fileTypes,callback){this.init(dropZoneElement,fileTypes,callback)}init(dropZoneElement,fileTypes,callback){return dropZoneElement.addEventListener("dragover",(e=>{const dropZone=this.getDropZoneFromEvent(e);dropZone&&(e.preventDefault(),dropZone.classList.add("dragover"))})),dropZoneElement.addEventListener("dragleave",(e=>{const dropZone=this.getDropZoneFromEvent(e);dropZone&&(e.preventDefault(),dropZone.classList.remove("dragover"))})),dropZoneElement.addEventListener("drop",(e=>{const dropZone=this.getDropZoneFromEvent(e);dropZone&&(e.preventDefault(),dropZone.classList.remove("dragover"),callback(e.dataTransfer.files))})),dropZoneElement.addEventListener("click",(e=>{this.getDropZoneContainerFromEvent(e)&&this.getFileElementFromEvent(e).click()})),dropZoneElement.addEventListener("click",(e=>{e.target.closest(".dropzone-sr-only-focusable")&&this.getFileElementFromEvent(e).click()})),dropZoneElement.addEventListener("change",(e=>{const fileInput=this.getFileElementFromEvent(e);fileInput&&(e.preventDefault(),callback(fileInput.files))})),this.renderDropZone(dropZoneElement,fileTypes),_log.default.info("Dropzone has been initialized!"),this}getDropZoneFromEvent(e){return e.target.closest(".dropzone")}getDropZoneContainerFromEvent(e){return e.target.closest(".dropzone-container")}getFileElementFromEvent(e){return e.target.closest(".dropzone-container").querySelector(".drop-zone-fileinput")}async renderDropZone(dropZoneElement,fileTypes){dropZoneElement.innerHTML=await _templates.default.render("core/dropzone",{fileTypes:fileTypes})}};return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=dropzone.min.js.map \ No newline at end of file diff --git a/lib/amd/build/dropzone.min.js.map b/lib/amd/build/dropzone.min.js.map new file mode 100644 index 00000000000..bbef7dda720 --- /dev/null +++ b/lib/amd/build/dropzone.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dropzone.min.js","sources":["../src/dropzone.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 * JavaScript to handle dropzone.\n *\n * @module core/dropzone\n * @copyright 2024 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.4\n */\n\nimport Log from 'core/log';\nimport Templates from 'core/templates';\n\n/**\n * A dropzone.\n *\n * @class core/dropzone\n */\nconst DropZone = class {\n\n /**\n * Constructor.\n *\n * @param {Element} dropZoneElement The element to render the dropzone.\n * @param {String} fileTypes The file types that are allowed to be uploaded. Example: image/*\n * @param {CallableFunction} callback The function to call when a file is dropped.\n */\n constructor(dropZoneElement, fileTypes, callback) {\n this.init(dropZoneElement, fileTypes, callback);\n }\n\n /**\n * Initialise the dropzone.\n *\n * @param {Element} dropZoneElement The element to render the dropzone.\n * @param {String} fileTypes The file types that are allowed to be uploaded. Example: image/*\n * @param {CallableFunction} callback The function to call when a file is dropped.\n * @returns {DropZone}\n */\n init(dropZoneElement, fileTypes, callback) {\n dropZoneElement.addEventListener('dragover', (e) => {\n const dropZone = this.getDropZoneFromEvent(e);\n if (!dropZone) {\n return;\n }\n e.preventDefault();\n dropZone.classList.add('dragover');\n });\n dropZoneElement.addEventListener('dragleave', (e) => {\n const dropZone = this.getDropZoneFromEvent(e);\n if (!dropZone) {\n return;\n }\n e.preventDefault();\n dropZone.classList.remove('dragover');\n });\n dropZoneElement.addEventListener('drop', (e) => {\n const dropZone = this.getDropZoneFromEvent(e);\n if (!dropZone) {\n return;\n }\n e.preventDefault();\n dropZone.classList.remove('dragover');\n callback(e.dataTransfer.files);\n });\n dropZoneElement.addEventListener('click', (e) => {\n const dropZoneContainer = this.getDropZoneContainerFromEvent(e);\n if (!dropZoneContainer) {\n return;\n }\n this.getFileElementFromEvent(e).click();\n });\n dropZoneElement.addEventListener('click', (e) => {\n const dropZoneLabel = e.target.closest('.dropzone-sr-only-focusable');\n if (!dropZoneLabel) {\n return;\n }\n this.getFileElementFromEvent(e).click();\n });\n dropZoneElement.addEventListener('change', (e) => {\n const fileInput = this.getFileElementFromEvent(e);\n if (fileInput) {\n e.preventDefault();\n callback(fileInput.files);\n }\n });\n this.renderDropZone(dropZoneElement, fileTypes);\n Log.info('Dropzone has been initialized!');\n return this;\n }\n\n /**\n * Get the dropzone.\n *\n * @param {Event} e The event.\n * @returns {HTMLElement|bool}\n */\n getDropZoneFromEvent(e) {\n return e.target.closest('.dropzone');\n }\n\n /**\n * Get the dropzone container.\n *\n * @param {Event} e The event.\n * @returns {HTMLElement|bool}\n */\n getDropZoneContainerFromEvent(e) {\n return e.target.closest('.dropzone-container');\n }\n\n /**\n * Get the file element.\n *\n * @param {Event} e The event.\n * @returns {HTMLElement|bool}\n */\n getFileElementFromEvent(e) {\n return e.target.closest('.dropzone-container').querySelector('.drop-zone-fileinput');\n }\n\n /**\n * Render the dropzone.\n *\n * @param {Element} dropZoneElement The element to render the dropzone.\n * @param {String} fileTypes The file types that are allowed to be uploaded.\n * @returns {Promise}\n */\n async renderDropZone(dropZoneElement, fileTypes) {\n dropZoneElement.innerHTML = await Templates.render('core/dropzone', {\n fileTypes,\n });\n }\n};\n\nexport default DropZone;\n"],"names":["constructor","dropZoneElement","fileTypes","callback","init","addEventListener","e","dropZone","this","getDropZoneFromEvent","preventDefault","classList","add","remove","dataTransfer","files","getDropZoneContainerFromEvent","getFileElementFromEvent","click","target","closest","fileInput","renderDropZone","info","querySelector","innerHTML","Templates","render"],"mappings":";;;;;;;;kLAgCiB,MASbA,YAAYC,gBAAiBC,UAAWC,eAC/BC,KAAKH,gBAAiBC,UAAWC,UAW1CC,KAAKH,gBAAiBC,UAAWC,iBAC7BF,gBAAgBI,iBAAiB,YAAaC,UACpCC,SAAWC,KAAKC,qBAAqBH,GACtCC,WAGLD,EAAEI,iBACFH,SAASI,UAAUC,IAAI,gBAE3BX,gBAAgBI,iBAAiB,aAAcC,UACrCC,SAAWC,KAAKC,qBAAqBH,GACtCC,WAGLD,EAAEI,iBACFH,SAASI,UAAUE,OAAO,gBAE9BZ,gBAAgBI,iBAAiB,QAASC,UAChCC,SAAWC,KAAKC,qBAAqBH,GACtCC,WAGLD,EAAEI,iBACFH,SAASI,UAAUE,OAAO,YAC1BV,SAASG,EAAEQ,aAAaC,WAE5Bd,gBAAgBI,iBAAiB,SAAUC,IACbE,KAAKQ,8BAA8BV,SAIxDW,wBAAwBX,GAAGY,WAEpCjB,gBAAgBI,iBAAiB,SAAUC,IACjBA,EAAEa,OAAOC,QAAQ,qCAIlCH,wBAAwBX,GAAGY,WAEpCjB,gBAAgBI,iBAAiB,UAAWC,UAClCe,UAAYb,KAAKS,wBAAwBX,GAC3Ce,YACAf,EAAEI,iBACFP,SAASkB,UAAUN,gBAGtBO,eAAerB,gBAAiBC,wBACjCqB,KAAK,kCACFf,KASXC,qBAAqBH,UACVA,EAAEa,OAAOC,QAAQ,aAS5BJ,8BAA8BV,UACnBA,EAAEa,OAAOC,QAAQ,uBAS5BH,wBAAwBX,UACbA,EAAEa,OAAOC,QAAQ,uBAAuBI,cAAc,6CAU5CvB,gBAAiBC,WAClCD,gBAAgBwB,gBAAkBC,mBAAUC,OAAO,gBAAiB,CAChEzB,UAAAA"} \ No newline at end of file diff --git a/lib/amd/src/dropzone.js b/lib/amd/src/dropzone.js new file mode 100644 index 00000000000..5425bcd41c6 --- /dev/null +++ b/lib/amd/src/dropzone.js @@ -0,0 +1,150 @@ +// 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 . + +/** + * JavaScript to handle dropzone. + * + * @module core/dropzone + * @copyright 2024 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 4.4 + */ + +import Log from 'core/log'; +import Templates from 'core/templates'; + +/** + * A dropzone. + * + * @class core/dropzone + */ +const DropZone = class { + + /** + * Constructor. + * + * @param {Element} dropZoneElement The element to render the dropzone. + * @param {String} fileTypes The file types that are allowed to be uploaded. Example: image/* + * @param {CallableFunction} callback The function to call when a file is dropped. + */ + constructor(dropZoneElement, fileTypes, callback) { + this.init(dropZoneElement, fileTypes, callback); + } + + /** + * Initialise the dropzone. + * + * @param {Element} dropZoneElement The element to render the dropzone. + * @param {String} fileTypes The file types that are allowed to be uploaded. Example: image/* + * @param {CallableFunction} callback The function to call when a file is dropped. + * @returns {DropZone} + */ + init(dropZoneElement, fileTypes, callback) { + dropZoneElement.addEventListener('dragover', (e) => { + const dropZone = this.getDropZoneFromEvent(e); + if (!dropZone) { + return; + } + e.preventDefault(); + dropZone.classList.add('dragover'); + }); + dropZoneElement.addEventListener('dragleave', (e) => { + const dropZone = this.getDropZoneFromEvent(e); + if (!dropZone) { + return; + } + e.preventDefault(); + dropZone.classList.remove('dragover'); + }); + dropZoneElement.addEventListener('drop', (e) => { + const dropZone = this.getDropZoneFromEvent(e); + if (!dropZone) { + return; + } + e.preventDefault(); + dropZone.classList.remove('dragover'); + callback(e.dataTransfer.files); + }); + dropZoneElement.addEventListener('click', (e) => { + const dropZoneContainer = this.getDropZoneContainerFromEvent(e); + if (!dropZoneContainer) { + return; + } + this.getFileElementFromEvent(e).click(); + }); + dropZoneElement.addEventListener('click', (e) => { + const dropZoneLabel = e.target.closest('.dropzone-sr-only-focusable'); + if (!dropZoneLabel) { + return; + } + this.getFileElementFromEvent(e).click(); + }); + dropZoneElement.addEventListener('change', (e) => { + const fileInput = this.getFileElementFromEvent(e); + if (fileInput) { + e.preventDefault(); + callback(fileInput.files); + } + }); + this.renderDropZone(dropZoneElement, fileTypes); + Log.info('Dropzone has been initialized!'); + return this; + } + + /** + * Get the dropzone. + * + * @param {Event} e The event. + * @returns {HTMLElement|bool} + */ + getDropZoneFromEvent(e) { + return e.target.closest('.dropzone'); + } + + /** + * Get the dropzone container. + * + * @param {Event} e The event. + * @returns {HTMLElement|bool} + */ + getDropZoneContainerFromEvent(e) { + return e.target.closest('.dropzone-container'); + } + + /** + * Get the file element. + * + * @param {Event} e The event. + * @returns {HTMLElement|bool} + */ + getFileElementFromEvent(e) { + return e.target.closest('.dropzone-container').querySelector('.drop-zone-fileinput'); + } + + /** + * Render the dropzone. + * + * @param {Element} dropZoneElement The element to render the dropzone. + * @param {String} fileTypes The file types that are allowed to be uploaded. + * @returns {Promise} + */ + async renderDropZone(dropZoneElement, fileTypes) { + dropZoneElement.innerHTML = await Templates.render('core/dropzone', { + fileTypes, + }); + } +}; + +export default DropZone; diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php index 95f4ee4d412..180d15def40 100644 --- a/lib/classes/output/icon_system_fontawesome.php +++ b/lib/classes/output/icon_system_fontawesome.php @@ -218,6 +218,7 @@ class icon_system_fontawesome extends icon_system_font { 'core:i/chartbar' => 'fa-chart-bar', 'core:i/course' => 'fa-graduation-cap', 'core:i/courseevent' => 'fa-graduation-cap', + 'core:i/cloudupload' => 'fa-cloud-upload', 'core:i/customfield' => 'fa-hand-o-right', 'core:i/db' => 'fa-database', 'core:i/delete' => 'fa-trash', diff --git a/lib/templates/dropzone.mustache b/lib/templates/dropzone.mustache new file mode 100644 index 00000000000..c7091833d49 --- /dev/null +++ b/lib/templates/dropzone.mustache @@ -0,0 +1,35 @@ +{{! + 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 . +}} +{{! + @template core/dropzone + + Render dropzone for file upload + + Example context (json): + { + "filetypes": "image/*,application/pdf" + } +}} +
+ {{# str }} addfiletext, repository {{/ str }} +
+
+ {{# pix }} i/cloudupload, core, {{# str }} addfilesdrop {{/ str }} {{/ pix }} +
+
+ {{# str }} addfilesdrop {{/ str }} +
+
+ +
diff --git a/pix/i/cloudupload.svg b/pix/i/cloudupload.svg new file mode 100644 index 00000000000..55e651ca028 --- /dev/null +++ b/pix/i/cloudupload.svg @@ -0,0 +1 @@ + diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index 80e5a3635ba..511890dc45b 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -2835,6 +2835,48 @@ body.dragging { cursor: move; } +.dropzone-container { + cursor: pointer; + + .dropzone { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border: 2px dashed $filemanager-dnd-border-color; + border-radius: 0.5rem; + + &.dragover { + border: 2px dashed $filemanager-dnd-upload-over-border-color; + } + } + + .dropzone-icon { + color: $gray-500; + + .icon { + font-size: 6em; + width: auto; + height: auto; + max-width: initial; + max-height: initial; + margin-right: 0; + } + } + + .dropzone-sr-only-focusable { + &:active, + &:focus { + outline: 0; + box-shadow: $input-btn-focus-box-shadow; + z-index: $zindex-popover; + position: relative; + background: $sr-only-active-bg; + padding: 7px; + } + } +} + // Generic classes reactive components can use. .overlay-preview { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index b7ee36ca085..a94165516cf 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -25725,6 +25725,40 @@ body.dragging .dragging { cursor: move; } +.dropzone-container { + cursor: pointer; +} +.dropzone-container .dropzone { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border: 2px dashed #bbb; + border-radius: 0.5rem; +} +.dropzone-container .dropzone.dragover { + border: 2px dashed #6c8cd3; +} +.dropzone-container .dropzone-icon { + color: #8f959e; +} +.dropzone-container .dropzone-icon .icon { + font-size: 6em; + width: auto; + height: auto; + max-width: initial; + max-height: initial; + margin-right: 0; +} +.dropzone-container .dropzone-sr-only-focusable:active, .dropzone-container .dropzone-sr-only-focusable:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(15, 108, 191, 0.75); + z-index: 1060; + position: relative; + background: #fff; + padding: 7px; +} + .overlay-preview { background-color: rgba(255, 255, 255, 0.8); border: 2px dashed #0f6cbf; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 20cd8dfe65d..e32f02e8e61 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -25725,6 +25725,40 @@ body.dragging .dragging { cursor: move; } +.dropzone-container { + cursor: pointer; +} +.dropzone-container .dropzone { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + border: 2px dashed #bbb; + border-radius: 0.5rem; +} +.dropzone-container .dropzone.dragover { + border: 2px dashed #6c8cd3; +} +.dropzone-container .dropzone-icon { + color: #8f959e; +} +.dropzone-container .dropzone-icon .icon { + font-size: 6em; + width: auto; + height: auto; + max-width: initial; + max-height: initial; + margin-right: 0; +} +.dropzone-container .dropzone-sr-only-focusable:active, .dropzone-container .dropzone-sr-only-focusable:focus { + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(15, 108, 191, 0.75); + z-index: 1060; + position: relative; + background: #fff; + padding: 7px; +} + .overlay-preview { background-color: rgba(255, 255, 255, 0.8); border: 2px dashed #0f6cbf;