diff --git a/README.md b/README.md index 7bf7b288..e93cb327 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,6 @@ to complete the installation from the command line. ## License ## -The included DTD for SVG is licensed by the World Wide Web Consortium, see copyright -notice in pix/svg11.dtd - 2021 ISB Bayern, Stefan Hanauska Icon by Dunja Speckner Dragging of SVG elements by Peter Collingridge diff --git a/amd/build/linkmodal.min.js b/amd/build/linkmodal.min.js new file mode 100644 index 00000000..b662f5aa --- /dev/null +++ b/amd/build/linkmodal.min.js @@ -0,0 +1,10 @@ +define("mod_learningmap/linkmodal",["exports","core/modal","core/ajax","core_course/manual_completion_toggle","mod_learningmap/renderer","core_course/events","jquery"],(function(_exports,_modal,_ajax,manualcompletion,_renderer,_events,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Helper for opening course modules in a modal that do not have a view page. + * + * @module mod_learningmap/linkmodal + * @copyright 2025 ISB Bayern + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_modal=_interopRequireDefault(_modal),_ajax=_interopRequireDefault(_ajax),manualcompletion=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(manualcompletion),_events=_interopRequireDefault(_events),_jquery=_interopRequireDefault(_jquery);_exports.init=async learningmapcmid=>{document.getElementById("learningmap-render-container-"+learningmapcmid).addEventListener("click",(async event=>{const target=event.target.closest("a[data-cmid]");if(target&&!target.hasAttribute("xlink:href")){event.preventDefault();const cmid=target.getAttribute("data-cmid");if(cmid){const data=await _ajax.default.call([{methodname:"mod_learningmap_get_cm",args:{cmid:cmid}}])[0];let js=_jquery.default.parseHTML(data.js,null,!0).map((node=>node.innerHTML)).join("\n");const modal=await _modal.default.create({title:data.name,body:data.completion+data.html,show:!1,removeOnClose:!0,large:!0});modal.bodyJS=js,modal.show(),manualcompletion.init(),document.addEventListener(_events.default.manualCompletionToggled,(()=>{(0,_renderer.renderLearningmap)(learningmapcmid)}))}}}))}})); + +//# sourceMappingURL=linkmodal.min.js.map \ No newline at end of file diff --git a/amd/build/linkmodal.min.js.map b/amd/build/linkmodal.min.js.map new file mode 100644 index 00000000..1b304973 --- /dev/null +++ b/amd/build/linkmodal.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"linkmodal.min.js","sources":["../src/linkmodal.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\nimport Modal from 'core/modal';\nimport Ajax from 'core/ajax';\nimport * as manualcompletion from 'core_course/manual_completion_toggle';\nimport {renderLearningmap} from 'mod_learningmap/renderer';\nimport CourseEvents from 'core_course/events';\nimport $ from 'jquery';\n\n/**\n * Helper for opening course modules in a modal that do not have a view page.\n *\n * @module mod_learningmap/linkmodal\n * @copyright 2025 ISB Bayern\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Initialize the link modal listener for a learning map.\n *\n * @param {number} learningmapcmid - The course module ID of the learning map.\n */\nexport const init = async(learningmapcmid) => {\n document.getElementById('learningmap-render-container-' + learningmapcmid).addEventListener('click', async(event) => {\n const target = event.target.closest('a[data-cmid]');\n if (target && !target.hasAttribute('xlink:href')) {\n event.preventDefault();\n const cmid = target.getAttribute('data-cmid');\n if (cmid) {\n const data = await Ajax.call([{\n methodname: 'mod_learningmap_get_cm',\n args: {\n cmid: cmid\n },\n }])[0];\n let js = $.parseHTML(data.js, null, true).map(node => node.innerHTML).join(\"\\n\");\n const modal = await Modal.create({\n title: data.name,\n body: data.completion + data.html,\n show: false,\n removeOnClose: true,\n large: true,\n });\n modal.bodyJS = js;\n modal.show();\n manualcompletion.init();\n document.addEventListener(CourseEvents.manualCompletionToggled, () => {\n renderLearningmap(learningmapcmid);\n });\n }\n }\n });\n};\n"],"names":["async","document","getElementById","learningmapcmid","addEventListener","target","event","closest","hasAttribute","preventDefault","cmid","getAttribute","data","Ajax","call","methodname","args","js","$","parseHTML","map","node","innerHTML","join","modal","Modal","create","title","name","body","completion","html","show","removeOnClose","large","bodyJS","manualcompletion","init","CourseEvents","manualCompletionToggled"],"mappings":";;;;;;;06BAmCoBA,MAAAA,kBAChBC,SAASC,eAAe,gCAAkCC,iBAAiBC,iBAAiB,SAASJ,MAAAA,cAC3FK,OAASC,MAAMD,OAAOE,QAAQ,mBAChCF,SAAWA,OAAOG,aAAa,cAAe,CAC9CF,MAAMG,uBACAC,KAAOL,OAAOM,aAAa,gBAC7BD,KAAM,OACAE,WAAaC,cAAKC,KAAK,CAAC,CAC1BC,WAAY,yBACZC,KAAM,CACFN,KAAMA,SAEV,OACAO,GAAKC,gBAAEC,UAAUP,KAAKK,GAAI,MAAM,GAAMG,KAAIC,MAAQA,KAAKC,YAAWC,KAAK,YACrEC,YAAcC,eAAMC,OAAO,CAC7BC,MAAOf,KAAKgB,KACZC,KAAMjB,KAAKkB,WAAalB,KAAKmB,KAC7BC,MAAM,EACNC,eAAe,EACfC,OAAO,IAEXV,MAAMW,OAASlB,GACfO,MAAMQ,OACNI,iBAAiBC,OACjBpC,SAASG,iBAAiBkC,gBAAaC,yBAAyB,qCAC1CpC"} \ No newline at end of file diff --git a/amd/src/linkmodal.js b/amd/src/linkmodal.js new file mode 100644 index 00000000..873b0a26 --- /dev/null +++ b/amd/src/linkmodal.js @@ -0,0 +1,66 @@ +// 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 . + +import Modal from 'core/modal'; +import Ajax from 'core/ajax'; +import * as manualcompletion from 'core_course/manual_completion_toggle'; +import {renderLearningmap} from 'mod_learningmap/renderer'; +import CourseEvents from 'core_course/events'; +import $ from 'jquery'; + +/** + * Helper for opening course modules in a modal that do not have a view page. + * + * @module mod_learningmap/linkmodal + * @copyright 2025 ISB Bayern + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Initialize the link modal listener for a learning map. + * + * @param {number} learningmapcmid - The course module ID of the learning map. + */ +export const init = async(learningmapcmid) => { + document.getElementById('learningmap-render-container-' + learningmapcmid).addEventListener('click', async(event) => { + const target = event.target.closest('a[data-cmid]'); + if (target && !target.hasAttribute('xlink:href')) { + event.preventDefault(); + const cmid = target.getAttribute('data-cmid'); + if (cmid) { + const data = await Ajax.call([{ + methodname: 'mod_learningmap_get_cm', + args: { + cmid: cmid + }, + }])[0]; + let js = $.parseHTML(data.js, null, true).map(node => node.innerHTML).join("\n"); + const modal = await Modal.create({ + title: data.name, + body: data.completion + data.html, + show: false, + removeOnClose: true, + large: true, + }); + modal.bodyJS = js; + modal.show(); + manualcompletion.init(); + document.addEventListener(CourseEvents.manualCompletionToggled, () => { + renderLearningmap(learningmapcmid); + }); + } + } + }); +}; diff --git a/classes/external/get_cm.php b/classes/external/get_cm.php new file mode 100644 index 00000000..09ec0cb4 --- /dev/null +++ b/classes/external/get_cm.php @@ -0,0 +1,122 @@ +. + +namespace mod_learningmap\external; + +use core_external\external_api; +use core_external\external_value; +use core_external\external_single_structure; +use core_external\external_function_parameters; +use core_courseformat\output\local\content\cm\completion; +/** + * Class get_cm + * + * @package mod_learningmap + * @copyright 2025 ISB Bayern + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class get_cm extends external_api { + /** + * Returns description of method parameters + * + * @return \external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters( + [ + 'cmid' => new external_value(PARAM_INT, 'Course module id'), + ] + ); + } + + /** + * Returns course module data + * + * @param int $cmid Course module id + * @return array Course module data + * @throws \moodle_exception + */ + public static function execute(int $cmid): array { + global $PAGE, $OUTPUT; + + define('LEARNINGMAP_NO_BACKLINK', true); + + $cm = get_coursemodule_from_id('', $cmid, 0, false, MUST_EXIST); + $context = \context_module::instance($cm->id); + $course = get_course($cm->course); + self::validate_context($context); + + require_capability('mod/' . $cm->modname . ':view', $context); + + $modinfo = get_fast_modinfo($course); + + if (!$cm->uservisible) { + require_capability('moodle/course:viewhiddenactivities', $context); + } + + $PAGE->set_url($cm->url ?? new \moodle_url('/mod/' . $cm->modname . '/view.php', ['id' => $cm->id])); + $PAGE->set_context($context); + $PAGE->set_cm($cm, $cm->course); + $PAGE->set_pagelayout('embedded'); + + $completioninfo = new \completion_info($course); + $completioninfo->set_module_viewed($cm); + + $data = (array)$cm; + + // Remove description for labels, because it is already in html. + if ($cm->modname === 'label') { + $PAGE->activityheader->set_description(''); + } + + $data['completion'] = $OUTPUT->render_from_template( + 'core/activity_header', + $PAGE->activityheader->export_for_template($OUTPUT) + ); + + $data['name'] = format_string($cm->name, true, ['context' => $context]); + + $PAGE->start_collecting_javascript_requirements(); + $data['html'] = $modinfo->get_cm($cmid)->get_formatted_content(['overflowdiv' => true, 'noclean' => true]); + $data['js'] = $PAGE->requires->get_end_code(); + + return $data; + } + + /** + * Returns description of method result value + * + * @return \external_description + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure( + [ + 'id' => new external_value(PARAM_INT, 'Course module id'), + 'course' => new external_value(PARAM_INT, 'Course id'), + 'module' => new external_value(PARAM_INT, 'Module id'), + 'instance' => new external_value(PARAM_INT, 'Instance id'), + 'section' => new external_value(PARAM_INT, 'Section id'), + 'name' => new external_value(PARAM_TEXT, 'Course module name'), + 'visible' => new external_value(PARAM_INT, 'Visible'), + 'groupmode' => new external_value(PARAM_INT, 'Group mode'), + 'groupingid' => new external_value(PARAM_INT, 'Grouping id'), + 'html' => new external_value(PARAM_RAW, 'Course module html'), + 'js' => new external_value(PARAM_RAW, 'Course module javascript'), + 'completion' => new external_value(PARAM_RAW, 'Completion html'), + ] + ); + } +} diff --git a/classes/helper.php b/classes/helper.php index 03017f93..39be7cf4 100644 --- a/classes/helper.php +++ b/classes/helper.php @@ -35,9 +35,19 @@ class helper { public static function show_map_on_course_page($cm): bool { global $DB; $showmaponcoursepage = $DB->get_field('learningmap', 'showmaponcoursepage', ['id' => $cm->instance]); + return !empty($showmaponcoursepage) && !self::is_learningmap_format($cm); + } + + /** + * Checks if the course format of the course the given cm belongs to is 'learningmap'. + * + * @param cm_info $cm The course module info object. + * @return bool True if the course format is 'learningmap', false otherwise. + */ + public static function is_learningmap_format($cm): bool { [$course, ] = get_course_and_cm_from_cmid($cm->id); $courseformat = $course->format; - return !empty($showmaponcoursepage) && $courseformat !== 'learningmap'; + return $courseformat === 'learningmap'; } /** @@ -89,4 +99,16 @@ public static function repair_learningmap_record(int $learningmapid): void { } } } + + /** + * Determines if the current request is an AJAX request for getting a course module. + * + * @return bool True if the request is an AJAX request for getting a course module, false otherwise. + */ + public static function is_ajax_request(): bool { + global $_REQUEST; + return + !empty($_REQUEST['info']) && + in_array($_REQUEST['info'], ['core_course_get_module', 'mod_learningmap_get_cm']); + } } diff --git a/classes/local/hook_callbacks.php b/classes/local/hook_callbacks.php index 1ccd5bad..fc19e720 100644 --- a/classes/local/hook_callbacks.php +++ b/classes/local/hook_callbacks.php @@ -50,6 +50,10 @@ class hook_callbacks { public static function inject_backlinks_into_activity_header(before_http_headers $beforehttpheadershook): void { global $OUTPUT, $PAGE; + if (defined('LEARNINGMAP_NO_BACKLINK')) { + return; + } + // Don't run during initial install. if (during_initial_install()) { return; diff --git a/classes/mapworker.php b/classes/mapworker.php index 110ffc55..28132133 100644 --- a/classes/mapworker.php +++ b/classes/mapworker.php @@ -66,7 +66,7 @@ class mapworker { * * @param string $svgcode The SVG code to build the map from * @param array $placestore The placestore data to use while processing the map - * @param cm_info|null $cm The course module that belongs to the map (null by default) + * @param \cm_info|null $cm The course module that belongs to the map (null by default) * @param bool $edit Whether the mapworker should prepare the map for edit mode (false by default) * @param int $group Group id to use (default 0 means no group) */ @@ -163,17 +163,25 @@ public function process_map_objects(): void { $placecm = $modinfo->get_cm($place['linkedActivity']); + $url = ''; // Set the link URL in the map. if (!empty($placecm->url)) { // Link modules that have a view page to their corresponding url. - $url = '' . $placecm->url; + $url = $placecm->url->out(); } else { - // Other modules (like labels) are shown on the course page. Link to the corresponding anchor. - $url = $CFG->wwwroot . '/course/view.php?id=' . $placecm->course . - '§ion=' . $placecm->sectionnum . '#module-' . $placecm->id; + // Other modules (like labels) are shown on the course page. + if (empty($this->placestore['usemodal'])) { + // Link to the corresponding anchor. + $url = $CFG->wwwroot . '/course/view.php?id=' . $placecm->course . + '§ion=' . $placecm->sectionnum . '#module-' . $placecm->id; + } } if (!$this->edit) { - $this->svgmap->set_link($place['linkId'], $url); + $this->svgmap->set_attribute($place['linkId'], 'data-cmid', $placecm->id); + $this->svgmap->remove_link($place['linkId']); + if (!empty($url)) { + $this->svgmap->set_link($place['linkId'], $url); + } } $links[$place['id']] = $place['linkId']; $this->svgmap->update_text_and_title( diff --git a/classes/svgmap.php b/classes/svgmap.php index eac5a5b4..5885e3a1 100644 --- a/classes/svgmap.php +++ b/classes/svgmap.php @@ -17,6 +17,9 @@ namespace mod_learningmap; use DOMDocument; +use DOMNode; +use DOMElement; +use DOMXPath; /** * Class for handling the content of the learningmap @@ -32,6 +35,11 @@ class svgmap { * @var DOMDocument */ protected DOMDocument $dom; + /** + * DOMXPath for querying the SVG + * @var DOMXPath + */ + protected DOMXPath $xpath; /** * String containing the SVG code (synchronized with $dom) * @var string @@ -54,23 +62,15 @@ class svgmap { * @param array $placestore The placestore data to use while processing the map */ public function __construct(string $svgcode, array $placestore) { - global $CFG; $this->svgcode = $svgcode; $this->placestore = $placestore; - // This fixes a problem for loading SVG DTD on Windows locally. - if (strcasecmp(substr(PHP_OS, 0, 3), 'WIN') == 0) { - $dtd = '' . new \moodle_url('/mod/learningmap/pix/svg11.dtd'); - } else { - $dtd = $CFG->dirroot . '/mod/learningmap/pix/svg11.dtd'; - } - $this->prepend = ''; $this->dom = new \DOMDocument('1.0', 'UTF-8'); - $this->dom->validateOnParse = true; $this->dom->preserveWhiteSpace = false; $this->dom->formatOutput = true; $this->load_dom(); + $this->xpath = new \DOMXPath($this->dom); } /** @@ -80,7 +80,7 @@ public function __construct(string $svgcode, array $placestore) { */ public function load_dom(): void { $this->remove_tags_before_svg(); - $this->dom->loadXML($this->prepend . $this->svgcode); + $this->dom->loadXML($this->svgcode); } /** @@ -121,7 +121,7 @@ public function replace_defs(): void { * @return void */ public function remove_tags_before_svg(): void { - $remove = ['', $this->prepend]; + $remove = ['']; $this->svgcode = str_replace($remove, '', $this->svgcode); } @@ -151,7 +151,7 @@ public function save_svg_data(): void { * @return ?string null, if element doesn't exist */ public function get_attribute(string $id, string $attribute): ?string { - $element = $this->dom->getElementById($id); + $element = $this->get_element_by_id($id); return $element === null ? null : $element->getAttribute($attribute); } @@ -162,7 +162,7 @@ public function get_attribute(string $id, string $attribute): ?string { * @return void */ public function remove_place_or_path(string $id): void { - $placeorpath = $this->dom->getElementById($id); + $placeorpath = $this->get_element_by_id($id); if ($placeorpath) { if ($placeorpath->nodeName == 'circle') { // Also remove connected paths for places. @@ -186,7 +186,7 @@ public function remove_place_or_path(string $id): void { * @return void */ public function set_link(string $linkid, string $url): void { - $link = $this->dom->getElementById($linkid); + $link = $this->get_element_by_id($linkid); if ($link) { $link->setAttribute('xlink:href', $url); } @@ -199,7 +199,7 @@ public function set_link(string $linkid, string $url): void { * @return void */ public function remove_link(string $linkid): void { - $link = $this->dom->getElementById($linkid); + $link = $this->get_element_by_id($linkid); if ($link) { $link->removeAttribute('xlink:href'); } @@ -216,12 +216,12 @@ public function remove_link(string $linkid): void { public function update_text_and_title(string $placeid, string $text, string $additionaltitle): void { // Set the title element for the link (for accessibility) and for a tooltip when hovering // the link. - $titlenode = $this->dom->getElementById('title' . $placeid); + $titlenode = $this->get_element_by_id('title' . $placeid); if ($titlenode) { $titlenode->nodeValue = $text . $additionaltitle; } // Set the text element for the link. - $textnode = $this->dom->getElementById('text' . $placeid); + $textnode = $this->get_element_by_id('text' . $placeid); if ($textnode) { $textnode->nodeValue = $text; } @@ -234,7 +234,7 @@ public function update_text_and_title(string $placeid, string $text, string $add * @return void */ public function set_hidden(string $id): void { - $placeorpath = $this->dom->getElementById($id); + $placeorpath = $this->get_element_by_id($id); if ($placeorpath) { $placeorpath->setAttribute('class', $placeorpath->getAttribute('class') . ' learningmap-hidden'); } @@ -247,7 +247,7 @@ public function set_hidden(string $id): void { * @return void */ public function set_reachable(string $id): void { - $placeorpath = $this->dom->getElementById($id); + $placeorpath = $this->get_element_by_id($id); if ($placeorpath) { $placeorpath->setAttribute('class', $placeorpath->getAttribute('class') . ' learningmap-reachable'); } @@ -260,7 +260,7 @@ public function set_reachable(string $id): void { * @return void */ public function set_visited(string $id): void { - $placeorpath = $this->dom->getElementById($id); + $placeorpath = $this->get_element_by_id($id); if ($placeorpath) { $placeorpath->setAttribute('class', $placeorpath->getAttribute('class') . ' learningmap-visited'); } @@ -273,7 +273,7 @@ public function set_visited(string $id): void { * @return void */ public function set_waygone(string $id): void { - $path = $this->dom->getElementById($id); + $path = $this->get_element_by_id($id); if ($path) { $path->setAttribute('class', $path->getAttribute('class') . ' learningmap-waygone'); } @@ -286,7 +286,7 @@ public function set_waygone(string $id): void { * @return void */ public function add_checkmark(string $placeid): void { - $place = $this->dom->getElementById($placeid); + $place = $this->get_element_by_id($placeid); if ($place) { $x = $place->getAttribute('cx'); $y = $place->getAttribute('cy'); @@ -306,8 +306,8 @@ public function add_checkmark(string $placeid): void { public function get_coordinates(): array { global $CFG; $coordinates = []; - $pathsgroup = $this->dom->getElementById('pathsGroup'); - $placesgroup = $this->dom->getElementById('placesGroup'); + $pathsgroup = $this->get_element_by_id('pathsGroup'); + $placesgroup = $this->get_element_by_id('placesGroup'); if (empty($this->placestore['hidepaths'])) { // Only processing quadratic bezier curves here as other paths are already handled // via the coordinates of the corresponding places. @@ -335,7 +335,7 @@ public function get_coordinates(): array { $cy = intval($placenode->getAttribute('cy')); $coordinates[] = ['x' => $cx, 'y' => $cy]; if ($this->placestore['showtext']) { - $text = $this->dom->getElementById('text' . $placenode->getAttribute('id')); + $text = $this->get_element_by_id('text' . $placenode->getAttribute('id')); if ($text) { // Delta of the text in relation to the places center coordinates. $dx = $text->getAttribute('dx'); @@ -361,7 +361,7 @@ public function get_coordinates(): array { public function add_overlay(): void { $coordinates = $this->get_coordinates(); if (count($coordinates) > 0) { - $backgroundnode = $this->dom->getElementById('learningmap-background-image'); + $backgroundnode = $this->get_element_by_id('learningmap-background-image'); $height = $backgroundnode->getAttribute('height'); $c = array_pop($coordinates); $minx = $c['x']; @@ -389,7 +389,7 @@ public function add_overlay(): void { $maxx = min(800, $maxx + $padding); $maxy = min($height, $maxy + $padding); - $placesgroup = $this->dom->getElementById('placesGroup'); + $placesgroup = $this->get_element_by_id('placesGroup'); // Create the overlay for slicemode. $overlay = $this->dom->createElement('path'); @@ -416,4 +416,96 @@ public function add_overlay(): void { $placesgroup->appendChild($overlay); } } + + /** + * Emulates getElementsByClassname via XPath + * + * @param string $classname The class name to search for + * @return array An array of matching elements + */ + public function get_elements_by_classname(string $classname): array { + $elements = $this->xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' $classname ')]"); + return iterator_to_array($elements); + } + + /** + * Emulates getElementById via XPath + * + * @param string $id The id to search for + * @return DOMElement|null The matching element or null if not found + */ + public function get_element_by_id(string $id): ?DOMElement { + $elements = $this->xpath->query("//*[@id='$id']"); + return $elements->length > 0 ? $elements->item(0) : null; + } + + /** + * Wraps all items with a specific class name in links. + * + * @param string $classname The class name to search for + * @param string $url The URL to link to + * @return void + */ + public function wrap_items_in_links(string $classname, string $url): void { + $elements = $this->get_elements_by_classname($classname); + foreach ($elements as $element) { + $this->wrap_element_in_link($element, $url); + } + } + + /** + * Removes all elements with a specific class name. + * + * @param string $classname The class name to search for + * @return void + */ + public function remove_elements_by_classname(string $classname): void { + $elements = $this->get_elements_by_classname($classname); + foreach ($elements as $element) { + $element->parentNode->removeChild($element); + } + } + + /** + * Wraps an item in a link. + * + * @param string $id Id of a place or path + * @param string $url URL to link to + * @return void + */ + public function wrap_in_link(string $id, string $url): void { + $element = $this->get_element_by_id($id); + if ($element) { + $this->wrap_element_in_link($element, $url); + } + } + + /** + * Wraps an element in a link. + * + * @param DOMNode $element The element to wrap + * @param string $url The URL to link to + * @return void + */ + public function wrap_element_in_link(DOMNode $element, string $url): void { + $link = $this->dom->createElement('a'); + $link->setAttribute('xlink:href', $url); + $element->parentNode->insertBefore($link, $element); + $link->appendChild($element); + } + + /** + * Sets an attribute of an element. + * + * @param string $id The id of the DOM element + * @param string $attribute The name of the attribute + * @param string $value The value to set the attribute to + * @return void + */ + public function set_attribute(string $id, string $attribute, string $value): void { + $element = $this->get_element_by_id($id); + if ($element) { + $element->setAttribute($attribute, $value); + } + } } diff --git a/db/install.xml b/db/install.xml index 6c968254..19483b24 100644 --- a/db/install.xml +++ b/db/install.xml @@ -17,6 +17,7 @@ + diff --git a/db/services.php b/db/services.php index b1ba2af5..247cb9fc 100644 --- a/db/services.php +++ b/db/services.php @@ -42,4 +42,12 @@ 'ajax' => true, 'capabilities' => 'mod/learningmap:view', ], + 'mod_learningmap_get_cm' => [ + 'classname' => 'mod_learningmap\external\get_cm', + 'methodname' => 'execute', + 'description' => 'Retrieves the course module data', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'mod/learningmap:view', + ], ]; diff --git a/db/upgrade.php b/db/upgrade.php index 87655018..2c05b64f 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -97,5 +97,19 @@ function xmldb_learningmap_upgrade($oldversion) { upgrade_mod_savepoint(true, 2024072201, 'learningmap'); } + if ($oldversion < 2025092200) { + // Define field usemodal to be added to learningmap. + $table = new xmldb_table('learningmap'); + $field = new xmldb_field('usemodal', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'showmaponcoursepage'); + + // Conditionally launch add field usemodal. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Learningmap savepoint reached. + upgrade_mod_savepoint(true, 2025092200, 'learningmap'); + } + return true; } diff --git a/lang/en/learningmap.php b/lang/en/learningmap.php index 1339f256..09c291fc 100644 --- a/lang/en/learningmap.php +++ b/lang/en/learningmap.php @@ -98,4 +98,6 @@ $string['usecaselink'] = 'Link to a page explaining the use of the learning map'; $string['usecheckmark'] = 'Checkmark for visited places'; $string['usecheckmark_help'] = 'This option additionally shows a checkmark in visited places.'; +$string['usemodal'] = 'Use modal to display labels'; +$string['usemodal_help'] = 'Show labels and label-like activities in a modal.'; $string['visited'] = 'Visited'; diff --git a/lib.php b/lib.php index 3a7d6ff3..c925ad9f 100644 --- a/lib.php +++ b/lib.php @@ -234,7 +234,7 @@ function learningmap_cm_info_dynamic(cm_info $cm): void { * @return void */ function learningmap_cm_info_view(cm_info $cm): void { - global $DB, $OUTPUT; + global $DB, $OUTPUT, $_REQUEST; $learningmap = $DB->get_record('learningmap', ['id' => $cm->instance]); $intro = ''; @@ -246,7 +246,7 @@ function learningmap_cm_info_view(cm_info $cm): void { } // Only show map on course page if showmaponcoursepage is set. - if (helper::show_map_on_course_page($cm)) { + if (helper::show_map_on_course_page($cm) || helper::is_ajax_request()) { if (!empty($cm->groupmode)) { $groupdropdown = groups_print_activity_menu( $cm, @@ -269,6 +269,13 @@ function learningmap_cm_info_view(cm_info $cm): void { $contentbeforemap = $groupdropdown . $intro; $hascontentbeforemap = !empty($contentbeforemap); + $mapcontent = null; + + if (helper::is_ajax_request()) { + // If this is an ajax request to get the cm, we need to return only the map code. + $mapcontent = learningmap_get_learningmap($cm); + } + $mapcontainer = $OUTPUT->render_from_template( 'mod_learningmap/rendercontainer', [ @@ -276,6 +283,8 @@ function learningmap_cm_info_view(cm_info $cm): void { 'enableLiveUpdater' => true, 'contentbeforemap' => $contentbeforemap, 'hascontentbeforemap' => $hascontentbeforemap, + 'mapcontent' => $mapcontent, + 'usemodal' => !empty($learningmap->usemodal) || helper::is_learningmap_format($cm), ] ); @@ -337,6 +346,8 @@ function learningmap_get_learningmap(cm_info $cm): string { $group = (empty($cm->groupmode) ? 0 : groups_get_activity_group($cm, true)); + $placestore['usemodal'] = $map->usemodal ?? 0; + $worker = new \mod_learningmap\mapworker($svg, $placestore, $cm, false, $group); $worker->process_map_objects(); $worker->remove_tags_before_svg(); diff --git a/mod_form.php b/mod_form.php index c23936ff..5a5bc776 100644 --- a/mod_form.php +++ b/mod_form.php @@ -132,6 +132,15 @@ public function definition(): void { $mform->addElement('hidden', 'backlink', 0); } + if ($learningmapformat) { + $mform->addElement('hidden', 'usemodal', 1); + } else { + $mform->addElement('advcheckbox', 'usemodal', get_string('usemodal', 'learningmap')); + $mform->addHelpButton('usemodal', 'usemodal', 'learningmap'); + $mform->setDefault('usemodal', 1); + } + $mform->setType('usemodal', PARAM_INT); + $mform->addElement( 'filemanager', 'backgroundfile', diff --git a/pix/svg11.dtd b/pix/svg11.dtd deleted file mode 100644 index 72e733b1..00000000 --- a/pix/svg11.dtd +++ /dev/null @@ -1,5664 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - -]]> - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - -]]> - - - -]]> - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - - - -]]> - - - -]]> - - - - - - -]]> - - - -]]> - - - - - - -]]> - - - -]]> - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - -]]> - - -]]> - - - diff --git a/templates/rendercontainer.mustache b/templates/rendercontainer.mustache index 81bd9da4..928cd010 100644 --- a/templates/rendercontainer.mustache +++ b/templates/rendercontainer.mustache @@ -34,7 +34,9 @@ "cmId": "123", "enableLiveUpdater": true, "contentbeforemap": "

Some content before the map

", - "hascontentbeforemap": true + "hascontentbeforemap": true, + "mapcontent": "...", + "usemodal": true, } }}
@@ -44,13 +46,21 @@
{{/hascontentbeforemap}}
- {{> mod_learningmap/loadingplaceholder}} + {{^mapcontent}}{{> mod_learningmap/loadingplaceholder}}{{/mapcontent}} + {{{mapcontent}}}
{{#js}} + {{^mapcontent}} require(['mod_learningmap/renderer'], function(renderer) { renderer.init({{cmId}}); }); + {{/mapcontent}} + {{#usemodal}} + require(['mod_learningmap/linkmodal'], function(linkmodal) { + linkmodal.init({{cmId}}); + }); + {{/usemodal}} {{#enableLiveUpdater}} require(['mod_learningmap/initliveupdater'], function(initliveupdater) { initliveupdater.init({{cmId}}); diff --git a/version.php b/version.php index 9c66ad5b..79686352 100644 --- a/version.php +++ b/version.php @@ -26,7 +26,7 @@ $plugin->component = 'mod_learningmap'; $plugin->release = '0.9.12'; -$plugin->version = 2025080700; +$plugin->version = 2025092600; $plugin->requires = 2022112800; $plugin->supported = [401, 500]; $plugin->maturity = MATURITY_STABLE; diff --git a/view.php b/view.php index 8182fff6..a6cfbe4b 100644 --- a/view.php +++ b/view.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_learningmap\helper; + require('../../config.php'); require_once('lib.php'); @@ -61,9 +63,18 @@ $OUTPUT->box($groupdropdown); } +$mapcontent = learningmap_get_learningmap($cm); + echo $OUTPUT->render_from_template( 'mod_learningmap/rendercontainer', - ['cmId' => $id, 'enableLiveUpdater' => false, 'contentbeforemap' => '', 'hascontentbeforemap' => false] + [ + 'cmId' => $id, + 'enableLiveUpdater' => false, + 'contentbeforemap' => '', + 'hascontentbeforemap' => false, + 'mapcontent' => $mapcontent, + 'usemodal' => !empty($map->usemodal) || helper::is_learningmap_format($cm), + ] ); echo $OUTPUT->footer();