Skip to content

Commit 29dcb44

Browse files
committed
MBS-10624: Restructure SVG, remove CDATA
1 parent e063c64 commit 29dcb44

15 files changed

Lines changed: 604 additions & 88 deletions

amd/build/learningmap.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

amd/build/learningmap.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

amd/src/learningmap.js

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
* Main module for the learningmap editor
1818
*
1919
* @module mod_learningmap/learningmap
20-
* @copyright 2025 ISB Bayern
20+
* @copyright 2021-2026 ISB Bayern
2121
* @author Stefan Hanauska <stefan.hanauska@csg-in.de>
2222
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2323
*/
2424
import {exception as displayException, saveCancel} from 'core/notification';
2525
import Templates from 'core/templates';
2626
import placestore from 'mod_learningmap/placestore';
2727
import * as Str from 'core/str';
28+
import {debounce} from 'core/utils';
2829

2930
const circleRadius = 10;
3031

@@ -108,13 +109,13 @@ export const init = async() => {
108109
if (activitySelector.value) {
109110
let text = document.getElementById('text' + elementForActivitySelector);
110111
if (text) {
111-
text.replaceChildren(svgdoc.createCDATASection(
112+
text.replaceChildren(svgdoc.createTextNode(
112113
activitySelector.querySelector('option[value="' + activitySelector.value + '"]').textContent
113114
));
114115
}
115116
let title = document.getElementById('title' + elementForActivitySelector);
116117
if (title) {
117-
title.replaceChildren(svgdoc.createCDATASection(
118+
title.replaceChildren(svgdoc.createTextNode(
118119
activitySelector.querySelector('option[value="' + activitySelector.value + '"]').textContent
119120
));
120121
}
@@ -306,11 +307,11 @@ export const init = async() => {
306307
dragel = el;
307308
if (el) {
308309
el.addEventListener('mousedown', startDrag);
309-
el.addEventListener('mousemove', drag);
310+
el.addEventListener('mousemove', debounce(drag, 5));
310311
el.addEventListener('mouseup', endDrag);
311312
el.addEventListener('mouseleave', endDrag);
312313
el.addEventListener('touchstart', startTouch);
313-
el.addEventListener('touchmove', drag);
314+
el.addEventListener('touchmove', debounce(drag, 5));
314315
el.addEventListener('touchend', endTouch);
315316
el.addEventListener('touchleave', endTouch);
316317
el.addEventListener('touchcancel', endTouch);
@@ -336,7 +337,7 @@ export const init = async() => {
336337
pathsToUpdateSecondPoint = placestore.getPathsWithSid(selectedElement.id);
337338
} else if (evt.target.nodeName == 'text') {
338339
selectedElement = evt.target;
339-
let place = selectedElement.parentNode.querySelector('.learningmap-place');
340+
let place = findPlaceForText(selectedElement.id);
340341
offset = getMousePosition(evt);
341342
offset.x -= parseInt(selectedElement.getAttributeNS(null, "dx")) + place.cx.baseVal.value;
342343
offset.y -= parseInt(selectedElement.getAttributeNS(null, "dy")) + place.cy.baseVal.value;
@@ -365,7 +366,7 @@ export const init = async() => {
365366
let cx = coord.x - offset.x;
366367
let cy = coord.y - offset.y;
367368
if (selectedElement.nodeName == 'text') {
368-
let place = selectedElement.parentNode.querySelector('.learningmap-place');
369+
let place = findPlaceForText(selectedElement.id);
369370
// Calculate the delta from the current mouse position to the corresponding place.
370371
// coord: current mouse position
371372
// offset: delta from the mouse position to the coordinates of the text node
@@ -641,7 +642,7 @@ export const init = async() => {
641642
// Default value for delta: Circle radius * 1.5 (as a padding)
642643
text.setAttribute('dx', circleRadius * 1.5);
643644
text.setAttribute('dy', circleRadius * 1.5);
644-
let textcontent = svgdoc.createCDATASection(content);
645+
let textcontent = svgdoc.createTextNode(content);
645646
text.replaceChildren(textcontent);
646647
return text;
647648
}
@@ -689,10 +690,9 @@ export const init = async() => {
689690
* @param {*} child child item to set the link on
690691
* @param {*} id id of the link
691692
* @param {*} title title of the link
692-
* @param {*} text text to describe the link
693693
* @returns {any}
694694
*/
695-
function link(child, id, title = null, text = null) {
695+
function link(child, id, title = null) {
696696
let link = document.createElementNS('http://www.w3.org/2000/svg', 'a');
697697
link.setAttribute('id', id);
698698
link.setAttribute('xlink:href', '');
@@ -701,9 +701,6 @@ export const init = async() => {
701701
if (title !== null) {
702702
link.appendChild(title);
703703
}
704-
if (text !== null) {
705-
link.appendChild(text);
706-
}
707704
return link;
708705
}
709706

@@ -718,7 +715,7 @@ export const init = async() => {
718715
titlenode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
719716
svgnode.appendChild(titlenode);
720717
}
721-
let titlecontent = svgdoc.createCDATASection(values.title);
718+
let titlecontent = svgdoc.createTextNode(values.title);
722719
titlenode.replaceChildren(titlecontent);
723720
titlenode.setAttribute('id', 'title-' + placestore.getMapid());
724721

@@ -727,7 +724,7 @@ export const init = async() => {
727724
descnode = document.createElementNS('http://www.w3.org/2000/svg', 'desc');
728725
svgnode.appendChild(descnode);
729726
}
730-
let desccontent = svgdoc.createCDATASection(values.description);
727+
let desccontent = svgdoc.createTextNode(values.description);
731728
descnode.replaceChildren(desccontent);
732729
descnode.setAttribute('id', 'desc-' + placestore.getMapid());
733730
}
@@ -760,6 +757,10 @@ export const init = async() => {
760757
*/
761758
function addPlace(event) {
762759
let placesgroup = document.getElementById('placesGroup');
760+
if (!placesgroup) {
761+
placesgroup = document.getElementById('placesGroup-' + placestore.getMapid());
762+
}
763+
const textgroup = document.getElementById('textsGroup-' + placestore.getMapid());
763764
let placeId = 'p' + placestore.getId();
764765
let linkId = 'a' + placestore.getId();
765766
var CTM = event.target.getScreenCTM();
@@ -772,10 +773,12 @@ export const init = async() => {
772773
link(
773774
circle(cx, cy, circleRadius, 'learningmap-place learningmap-draggable learningmap-emptyplace', placeId),
774775
linkId,
775-
title('title' + placeId),
776-
text('text' + placeId, '', cx, cy)
776+
title('title' + placeId)
777777
)
778778
);
779+
textgroup.appendChild(
780+
text('text' + placeId, '', cx, cy)
781+
);
779782
placestore.addPlace(placeId, linkId);
780783
}
781784

@@ -840,6 +843,9 @@ export const init = async() => {
840843
let pid = 'p' + fid + '_' + sid;
841844
if (document.getElementById(pid) === null) {
842845
let pathsgroup = document.getElementById('pathsGroup');
846+
if (!pathsgroup) {
847+
pathsgroup = document.getElementById('pathsGroup-' + placestore.getMapid());
848+
}
843849
let first = document.getElementById('p' + fid);
844850
let second = document.getElementById('p' + sid);
845851
if (pathsgroup && first && second) {
@@ -871,7 +877,10 @@ export const init = async() => {
871877
placestore.removePlace(event.target.id);
872878
parent.removeChild(place);
873879
parent.parentNode.removeChild(parent);
874-
880+
let textNode = document.getElementById('text' + event.target.id);
881+
if (textNode) {
882+
textNode.parentNode.removeChild(textNode);
883+
}
875884
updateCode();
876885
}
877886

@@ -906,6 +915,9 @@ export const init = async() => {
906915
let previewimage = document.getElementsByClassName('realpreview');
907916
if (previewimage.length > 0) {
908917
let background = document.getElementById('learningmap-background-image');
918+
if (!background) {
919+
background = document.getElementById('learningmap-background-image-' + placestore.getMapid());
920+
}
909921
let backgroundurl = previewimage[0].getAttribute('src').split('?')[0];
910922
// If the uploaded file reuses the filename of a previously uploaded image, they differ
911923
// only in the oid. So one has to append the oid to the url.
@@ -922,6 +934,9 @@ export const init = async() => {
922934
*/
923935
function registerBackgroundListener() {
924936
let background = document.getElementById('learningmap-background-image');
937+
if (!background) {
938+
background = document.getElementById('learningmap-background-image-' + placestore.getMapid());
939+
}
925940
if (background) {
926941
background.addEventListener('load', function() {
927942
background.removeAttribute('height');
@@ -1069,8 +1084,9 @@ export const init = async() => {
10691084
}
10701085
}
10711086
let placeNode = document.getElementById(place.id);
1087+
let textGroup = document.getElementById('textsGroup-' + placestore.getMapid());
10721088
let textNode = text('text' + place.id, content, placeNode.cx.baseVal.value, placeNode.cy.baseVal.value);
1073-
placeNode.parentNode.appendChild(textNode);
1089+
textGroup.appendChild(textNode);
10741090
}
10751091
}
10761092
}
@@ -1082,4 +1098,14 @@ export const init = async() => {
10821098
let advancedSettings = document.getElementById('learningmap-advanced-settings');
10831099
advancedSettings.setAttribute('hidden', '');
10841100
}
1101+
1102+
/**
1103+
* Returns the place that belongs to the given text id.
1104+
* @param {*} textId
1105+
* @returns {*} The place element
1106+
*/
1107+
function findPlaceForText(textId) {
1108+
let placename = textId.replace('text', '');
1109+
return svgnode.getElementById(placename);
1110+
}
10851111
};

backup/moodle2/restore_learningmap_activity_task.class.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ public function after_restore(): void {
104104
$newmapid = uniqid();
105105
$placestore->mapid = $newmapid;
106106

107-
if (!isset($placestore->version) || $placestore->version < 2024072201) {
108-
$placestore->version = 2024072201;
107+
if (!isset($placestore->version) || $placestore->version < 2026022200) {
108+
$placestore->version = 2026022200;
109109
// Needs 1 as default value (otherwise all place strokes would be hidden).
110110
if (!isset($placestore->strokeopacity)) {
111111
$placestore->strokeopacity = 1;
@@ -121,9 +121,11 @@ public function after_restore(): void {
121121
$mapworker = new \mod_learningmap\mapworker($mapcode, (array)$placestore);
122122
$mapworker->replace_stylesheet();
123123
$mapworker->replace_defs();
124+
$mapworker->fix_svg();
124125
$item->svgcode = $mapworker->get_svgcode();
125126
}
126-
$item->svgcode = str_replace('learningmap-svgmap-' . $oldmapid, 'learningmap-svgmap-' . $newmapid, $item->svgcode);
127+
// Map ids are used in the svg code as well, so we need to replace them there too.
128+
$item->svgcode = str_replace('-' . $oldmapid, '-' . $newmapid, $item->svgcode);
127129
$item->placestore = json_encode($placestore);
128130
$item->course = $courseid;
129131
$DB->update_record('learningmap', $item);

backup/moodle2/restore_learningmap_stepslib.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ protected function define_structure(): array {
3535
}
3636

3737
/**
38-
* Restore a learningmap record.
38+
* Restore a learningmap record. This will not change placestore and
39+
* svgcode as they will be processed in the after_restore method.
3940
* @param array|object $data
4041
* @throws base_step_exception
4142
* @throws dml_exception

classes/mapworker.php

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,6 @@ public function __construct(
7878
int $group = 0
7979
) {
8080
global $USER;
81-
$svgcode = preg_replace('/(<\!--\[CDATA\[)(.*?)(\]\]-->)/', '<![CDATA[$2]]>', $svgcode);
82-
$svgcode = preg_replace(
83-
'/<text([^>]*)>(?!(<\!\[CDATA\[))(.*?)<\/text>/',
84-
'<text$1><![CDATA[$3]]></text>',
85-
$svgcode
86-
);
87-
$svgcode = preg_replace(
88-
'/<title([^>]*)>(?!(<\!\[CDATA\[))(.*?)<\/title>/',
89-
'<title$1><![CDATA[$3]]></title>',
90-
$svgcode
91-
);
92-
$svgcode = preg_replace(
93-
'/<desc([^>]*)>(?!(<\!\[CDATA\[))(.*?)<\/desc>/',
94-
'<desc$1><![CDATA[$3]]></desc>',
95-
$svgcode
96-
);
9781
$this->edit = $edit;
9882
$placestore['editmode'] = $this->edit;
9983
$this->placestore = $placestore;
@@ -122,7 +106,7 @@ public function replace_stylesheet(array $placestoreoverride = []): void {
122106
* @return void
123107
*/
124108
public function replace_defs(): void {
125-
$this->svgmap->replace_defs();
109+
$this->svgmap->replace_defs(['mapid' => $this->placestore['mapid']]);
126110
}
127111

128112
/**
@@ -324,7 +308,6 @@ public function is_path_between(string $place1, string $place2): ?string {
324308
* @return string
325309
*/
326310
public function get_svgcode(): string {
327-
$this->svgmap->replace_cdata();
328311
return $this->svgmap->get_svgcode();
329312
}
330313

@@ -346,4 +329,14 @@ public function get_attribute(string $id, string $attribute): ?string {
346329
public function get_active(): array {
347330
return $this->active;
348331
}
332+
333+
/**
334+
* Helper function for upgrade. Fixes the SVG code of existing maps to ensure that the groups for paths,
335+
* places and background are present and have the correct attributes.
336+
*
337+
* @return void
338+
*/
339+
public function fix_svg(): void {
340+
$this->svgmap->fix_svg();
341+
}
349342
}

0 commit comments

Comments
 (0)