From db22f089975c0bd87949140965a837433b6a4587 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 18 Jul 2022 17:18:34 +0200 Subject: [PATCH 01/60] feat (multiple): allow empty annotations, remove annotation type type for participants, width of annotation area now changable --- annotations.php | 4 ---- classes/output/margic_view.php | 12 +++++++++++- db/install.xml | 1 + db/upgrade.php | 16 ++++++++++++++++ lang/de/margic.php | 5 ++++- lang/en/margic.php | 5 ++++- mod_form.php | 28 +++++++++++++++++++++++++--- settings.php | 4 ++++ templates/margic_view.mustache | 23 ++++++++++++++--------- version.php | 4 ++-- view.php | 13 ++++++++++--- 11 files changed, 91 insertions(+), 24 deletions(-) diff --git a/annotations.php b/annotations.php index 3a0409f..dd25cf9 100644 --- a/annotations.php +++ b/annotations.php @@ -125,10 +125,6 @@ if ($fromform->startcontainer != -1 && $fromform->endcontainer != -1 && $fromform->startposition != -1 && $fromform->endposition != -1) { - if ($fromform->text == '') { - redirect($redirecturl, get_string('erremptyannotation', 'mod_margic'), null, notification::NOTIFY_ERROR); - } - if (!isset($fromform->type)) { redirect($redirecturl, get_string('errtypedeleted', 'mod_margic'), null, notification::NOTIFY_ERROR); } diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 8834b55..5386a10 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -56,6 +56,10 @@ class margic_view implements renderable, templatable { protected $entrybgc; /** @var string */ protected $entrytextbgc; + /** @var int */ + protected $entryareawidth; + /** @var int */ + protected $annotationareawidth; /** @var bool */ protected $caneditentries; /** @var int */ @@ -93,6 +97,8 @@ class margic_view implements renderable, templatable { * @param string $sortmode Sort mode for the margic instance * @param string $entrybgc Background color of the entries * @param string $entrytextbgc Background color of the texts in the entries + * @param int $entryareawidth Width of the entry area + * @param int $annotationareawidth Width of the annotation area * @param bool $caneditentries If own entries can be edited * @param int $edittimeends Time when entries cant be edited anymore * @param bool $edittimehasended If edit time has ended @@ -109,7 +115,7 @@ class margic_view implements renderable, templatable { * @param bool $canmakeannotations If user can make annotations * @param array $annotationtypes Array with annotation types for form */ - public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $caneditentries, $edittimeends, $edittimehasended, $canmanageentries, + public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, $caneditentries, $edittimeends, $edittimehasended, $canmanageentries, $sesskey, $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, $canmakeannotations, $annotationtypes) { $this->cm = $cm; @@ -120,6 +126,8 @@ public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $this->sortmode = $sortmode; $this->entrybgc = $entrybgc; $this->entrytextbgc = $entrytextbgc; + $this->annotationareawidth = $annotationareawidth; + $this->entryareawidth = 100 - $annotationareawidth; $this->caneditentries = $caneditentries; $this->edittimeends = $edittimeends; $this->edittimehasended = $edittimehasended; @@ -192,6 +200,8 @@ public function export_for_template(renderer_base $output) { $data->sortmode = $this->sortmode; $data->entrybgc = $this->entrybgc; $data->entrytextbgc = $this->entrytextbgc; + $data->entryareawidth = $this->entryareawidth; + $data->annotationareawidth = $this->annotationareawidth; $data->caneditentries = $this->caneditentries; $data->edittimeends = $this->edittimeends; $data->edittimehasended = $this->edittimehasended; diff --git a/db/install.xml b/db/install.xml index f076917..0932fac 100644 --- a/db/install.xml +++ b/db/install.xml @@ -22,6 +22,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index b8673b0..bc3d32a 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -49,5 +49,21 @@ function xmldb_margic_upgrade($oldversion) { } + if ($oldversion < 2022071801) { + + // Add the annotationareawidth field to the margic table. + $table = new xmldb_table('margic'); + $field = new xmldb_field('annotationareawidth', XMLDB_TYPE_INTEGER, '3', null, null, null, null, 'editdates'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Margic savepoint reached. + upgrade_mod_savepoint(true, 2022071801, 'margic'); + + } + return true; } diff --git a/lang/de/margic.php b/lang/de/margic.php index 5b6ba3d..186b3c6 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -218,7 +218,6 @@ $string['annotationcolor'] = 'Farbe des Fehlertyps'; $string['defaulttype'] = 'Standard Fehlertyp'; $string['customtype'] = 'Eigener Fehlertyp'; -$string['erremptyannotation'] = 'Text fehlt. Annotierung nicht gespeichert.'; $string['errnohexcolor'] = 'Kein hexadezimaler Farbwert.'; $string['changesforall'] = 'Die Änderung des Namens oder der Farbe des Fehlertypen wirkt sich sofort nach dem Speichern auf alle bereits Angelegten sowie alle zukünftigen Annotationen aus.'; $string['explanationtypename'] = 'Name des Fehlertyps'; @@ -236,6 +235,10 @@ $string['errfeedbacknotupdated'] = 'Rückmeldung und Note konnte nicht aktualisiert werden'; $string['errnograder'] = 'Kein Bewerter.'; $string['errnofeedbackorratingdisabled'] = 'Keine Rückmeldung oder Rückmeldung ist deaktiviert.'; +$string['annotationareawidth'] = 'Breite des Annotationsbereichs'; +$string['annotationareawidthall'] = 'Die Breite des Annotationsbereiches in Prozent für alle Margics. Kann von Lehrenden in den einzelnen Margics überschrieben werden.'; +$string['annotationareawidth_help'] = 'Die Breite des Annotationsbereiches in Prozent.'; +$string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index bcefb25..f46bb0f 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -231,7 +231,6 @@ $string['annotationcolor'] = 'Color of the annotation type'; $string['defaulttype'] = 'Default error type'; $string['customtype'] = 'Custom error type'; -$string['erremptyannotation'] = 'Text missing. Annotation not saved.'; $string['errnohexcolor'] = 'No hex value for color.'; $string['changesforall'] = 'Changing the name or color of the annotation type will affect all already created annotations as well as all future annotations immediately after saving.'; $string['explanationtypename'] = 'Name of annotation type'; @@ -249,6 +248,10 @@ $string['errfeedbacknotupdated'] = 'Feedback and grade not updated'; $string['errnograder'] = 'No grader.'; $string['errnofeedbackorratingdisabled'] = 'No feedback or rating disabled.'; +$string['annotationareawidth'] = 'Width of the annotation area'; +$string['annotationareawidthall'] = 'The width of the annotation area in percent for all margics. Can be overridden by teachers in the individual margics.'; +$string['annotationareawidth_help'] = 'The width of the annotation area in percent.'; +$string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/mod_form.php b/mod_form.php index 59bfb9b..6d8b040 100644 --- a/mod_form.php +++ b/mod_form.php @@ -54,7 +54,7 @@ public function definition() { $this->standard_intro_elements(get_string('margicdescription', 'margic')); - // Add the availability header. + // Add the header for availability. $mform->addElement('header', 'availibilityhdr', get_string('availability')); // 20200915 Moved check so daysavailable is hidden unless using weekly format. @@ -89,17 +89,39 @@ public function definition() { )); $mform->addHelpButton('timeclose', 'margicclosetime', 'margic'); - // 20201015 Added Edit all, enable/disable setting. + // Edit all setting if user can edit its own entries. $mform->addElement('selectyesno', 'editall', get_string('editall', 'margic')); $mform->addHelpButton('editall', 'editall', 'margic'); - // 20201119 Added Edit dates, enable/disable setting. + // Edit dates setting if user can modify entry date. $mform->addElement('selectyesno', 'editdates', get_string('editdates', 'margic')); $mform->addHelpButton('editdates', 'editdates', 'margic'); + // Add the header for appearance. + $mform->addElement('header', 'appearancehdr', get_string('appearance')); + + // Width of the annotation area. + $mform->addElement('text', 'annotationareawidth', get_string('annotationareawidth', 'margic')); + $mform->setType('annotationareawidth', PARAM_INT); + $mform->addHelpButton('annotationareawidth', 'annotationareawidth', 'margic'); + $mform->setDefault('annotationareawidth', get_config('mod_margic', 'annotationareawidth')); + // Add the rest of the common settings. $this->standard_grading_coursemodule_elements(); $this->standard_coursemodule_elements(); $this->add_action_buttons(); } + + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + $minwidth = 20; + $maxwidth = 80; + + if (!$data['annotationareawidth'] || $data['annotationareawidth'] < $minwidth || $data['annotationareawidth'] > $maxwidth) { + $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'margic', array('minwidth' => $minwidth, 'maxwidth' => $maxwidth)); + } + + return $errors; + } } diff --git a/settings.php b/settings.php index 8baf58b..2540bb6 100644 --- a/settings.php +++ b/settings.php @@ -62,6 +62,10 @@ $settings->add(new admin_setting_heading('mod_margic/appearance', get_string('appearance'), '')); + // Default width of annotation area. + $settings->add(new admin_setting_configtext('mod_margic/annotationareawidth', get_string('annotationareawidth', 'margic'), + get_string('annotationareawidthall', 'margic'), 40, '/^([2-7]\d|80)+$/')); // Range allowed: 20-80 + // Date format setting. $settings->add(new admin_setting_configtext('mod_margic/dateformat', get_string('dateformat', 'margic'), diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 5ecdcc3..e6511cb 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -94,7 +94,7 @@ {{#entries}}
-
+

{{#str}}entry, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}}

@@ -149,7 +149,7 @@
{{#annotationmode}} -
+

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}}
@@ -165,18 +165,23 @@
{{type}} - {{#defaulttype}} - (S) - {{/defaulttype}} - {{^defaulttype}} - (M) - {{/defaulttype}} + {{#canmanageentries}} + {{#defaulttype}} + (S) + {{/defaulttype}} + {{^defaulttype}} + (M) + {{/defaulttype}} + {{/canmanageentries}}
{{#str}}annotatedtextnotfound, mod_margic {{/str}}
- {{text}} + + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + {{#canbeedited}} {{/canbeedited}} diff --git a/version.php b/version.php index 51437f7..539bdba 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_margic'; -$plugin->release = '1.1.2'; // User-friendly version number. -$plugin->version = 2022071300; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '1.1.3'; // User-friendly version number. +$plugin->version = 2022071801; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; diff --git a/view.php b/view.php index 419e059..841bac6 100644 --- a/view.php +++ b/view.php @@ -190,14 +190,21 @@ $edittimeends = $moduleinstance->timeclose; } +if (isset($moduleinstance->annotationareawidth)) { + $annotationareawidth = $moduleinstance->annotationareawidth; +} else { + $annotationareawidth = get_config('mod_margic', 'annotationareawidth'); +} + // Handle groups. echo groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/margic/view.php?id=$id"); // Output page. $page = new margic_view($cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), $margic->get_sortmode(), - get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $moduleinstance->editall, - $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, $ratingaggregationmode, $course, - $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), $annotationmode, $canmakeannotations, $margic->get_annotationtypes_for_form()); + get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $annotationareawidth, + $moduleinstance->editall, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, + $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), + $annotationmode, $canmakeannotations, $margic->get_annotationtypes_for_form()); echo $OUTPUT->render($page); From b5f7f4724ea72d66f7ab68e8547736bf18e642d2 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 18 Jul 2022 17:47:31 +0200 Subject: [PATCH 02/60] feat (annotation): shortened timestamp --- lang/de/margic.php | 4 ++-- lang/en/margic.php | 4 ++-- templates/margic_view.mustache | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lang/de/margic.php b/lang/de/margic.php index 186b3c6..f884bea 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -211,8 +211,8 @@ $string['annotationtypedeleted'] = 'Fehlertyp entfernt'; $string['annotationtypeinvalid'] = 'Fehlertyp ungültig'; $string['nameofannotationtype'] = 'Name des Fehlertyps'; -$string['annotationcreated'] = 'Erstellt'; -$string['annotationmodified'] = 'Bearbeitet'; +$string['annotationcreated'] = 'Erstellt am {$a}'; +$string['annotationmodified'] = 'Bearbeitet am {$a}'; $string['editannotation'] = 'Bearbeiten'; $string['deleteannotation'] = 'Löschen'; $string['annotationcolor'] = 'Farbe des Fehlertyps'; diff --git a/lang/en/margic.php b/lang/en/margic.php index f46bb0f..c59c67b 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -224,8 +224,8 @@ $string['annotationtypedeleted'] = 'Annotation type deleted'; $string['annotationtypeinvalid'] = 'Annotation type invalid'; $string['nameofannotationtype'] = 'Name of annotation type'; -$string['annotationcreated'] = 'Created'; -$string['annotationmodified'] = 'Modified'; +$string['annotationcreated'] = 'Created at {$a}'; +$string['annotationmodified'] = 'Modified at {$a}'; $string['editannotation'] = 'Edit'; $string['deleteannotation'] = 'Delete'; $string['annotationcolor'] = 'Color of the annotation type'; diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index e6511cb..b463aba 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -158,8 +158,8 @@ {{{userpicturestr}}} - {{^timemodified}} {{#userdate}}{{timecreated}}, {{#str}} strftimedatetime, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}} {{#userdate}}{{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}}{{/userdate}}{{/timemodified}} + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}}
From 82dd81e20c7b0df5bc21dead7d2445bbd69d6119 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 19 Jul 2022 12:44:30 +0200 Subject: [PATCH 03/60] feat (annotation): reworked annotation ui --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 3 ++ lang/de/margic.php | 2 ++ lang/en/margic.php | 2 ++ styles.css | 29 +++++++++++------ templates/margic_view.mustache | 53 +++++++++++++++----------------- 7 files changed, 52 insertions(+), 41 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 56283fd..acd301a 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,2 +1,2 @@ -function _createForOfIteratorHelper(a){if("undefined"==typeof Symbol||null==a[Symbol.iterator]){if(Array.isArray(a)||(a=_unsupportedIterableToArray(a))){var b=0,c=function(){};return{s:c,n:function n(){if(b>=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c=a.comparePoint(b,0)&&0<=a.comparePoint(b,e)}catch(a){return!1}}function j(a){var b=a.nodeName.toLowerCase(),c=b;if("#text"===b){c="text()"}return c}function k(a){var b=0,c=a;while(c){if(c.nodeName===a.nodeName){b+=1}c=c.previousSibling}return b}function l(a){var b=j(a),c=k(a);return"".concat(b,"[").concat(c,"]")}function m(a,b){var c="",d=a;while(d!==b){if(!d){throw new Error("Node is not a descendant of root")}c=l(d)+"/"+c;d=d.parentNode}c="/"+c;c=c.replace(/\/$/,"");return c}function n(a,b,c){b=b.toUpperCase();for(var d=-1,e=0,f;ej){return null}}else{i=h;j=0}var m=n(e,i,j);if(!m){return null}e=m}}catch(a){f.e(a)}finally{f.f()}return e}function p(a){var b=1=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c=a.comparePoint(b,0)&&0<=a.comparePoint(b,e)}catch(a){return!1}}function j(a){var b=a.nodeName.toLowerCase(),c=b;if("#text"===b){c="text()"}return c}function k(a){var b=0,c=a;while(c){if(c.nodeName===a.nodeName){b+=1}c=c.previousSibling}return b}function l(a){var b=j(a),c=k(a);return"".concat(b,"[").concat(c,"]")}function m(a,b){var c="",d=a;while(d!==b){if(!d){throw new Error("Node is not a descendant of root")}c=l(d)+"/"+c;d=d.parentNode}c="/"+c;c=c.replace(/\/$/,"");return c}function n(a,b,c){b=b.toUpperCase();for(var d=-1,e=0,f;ej){return null}}else{i=h;j=0}var m=n(e,i,j);if(!m){return null}e=m}}catch(a){f.e(a)}finally{f.f()}return e}function p(a){var b=1.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"file":"annotations.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/annotations.js"],"names":["define","$","init","annotations","canmakeannotations","hide","removeClass","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","node","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","childNodes","comparePoint","e","getNodeName","nodeName","toLowerCase","result","getNodePosition","pos","tmp","previousSibling","getPathSegment","name","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","child","children","evaluateSimpleXPath","isSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","removeHighlights","pn","normalize","on","selectedrange","window","getSelection","getRangeAt","cloneContents","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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","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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .annotationtype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .annotationtype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"file":"annotations.min.js"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 5a83f73..68f89a4 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -525,12 +525,15 @@ var id = this.id.replace('annotated-', ''); $('.annotationpreview-'+id).addClass('hovered'); $('.annotated-'+id).addClass('hovered'); + $('.annotation-box-' + id + ' .annotationtype').addClass('hovered'); + }); $('.annotated').mouseleave (function() { var id = this.id.replace('annotated-', ''); $('.annotationpreview-'+id).removeClass('hovered'); $('.annotated-'+id).removeClass('hovered'); + $('.annotation-box-' + id + ' .annotationtype').removeClass('hovered'); }); // Highlight annotated text if annotationpreview is hovered diff --git a/lang/de/margic.php b/lang/de/margic.php index f884bea..f1f3429 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -239,6 +239,8 @@ $string['annotationareawidthall'] = 'Die Breite des Annotationsbereiches in Prozent für alle Margics. Kann von Lehrenden in den einzelnen Margics überschrieben werden.'; $string['annotationareawidth_help'] = 'Die Breite des Annotationsbereiches in Prozent.'; $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; +$string['toggleannotation'] = 'Annotation aus- / einklappen'; +$string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index c59c67b..603f82c 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -252,6 +252,8 @@ $string['annotationareawidthall'] = 'The width of the annotation area in percent for all margics. Can be overridden by teachers in the individual margics.'; $string['annotationareawidth_help'] = 'The width of the annotation area in percent.'; $string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; +$string['toggleannotation'] = 'Toggle annotation'; +$string['toggleallannotations'] = 'Toggle all annotations'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/styles.css b/styles.css index 0388732..687d136 100644 --- a/styles.css +++ b/styles.css @@ -146,7 +146,8 @@ #page-mod-margic-view .annotated:hover, #page-mod-margic-view .annotated_temp:hover, -#page-mod-margic-view .hovered { +#page-mod-margic-view .hovered, +#page-mod-margic-view .annotationtypeheader .hovered { background-color: lightblue !important; } @@ -154,8 +155,8 @@ background-color: white; border: 1px solid #dbdbdb; border-radius: 2px; - padding: 1em; - margin-bottom: 1em; + padding: 10px; + margin-bottom: 10px; box-shadow: 0 1px 1px rgba(0, 0, 0, .1); } @@ -176,21 +177,29 @@ } #page-mod-margic-view .annotatedtextpreviewdiv { + margin-top: 5px; + margin-bottom: 5px; +} + +#page-mod-margic-view .annotationauthor { + padding-top: 5px; + padding-bottom: 5px; margin-top: 10px; - margin-bottom: 10px; } #page-mod-margic-view .annotatedtextpreview { border-left: 5px solid yellow; padding-left: 5px; background-color: white; - margin-left: 5px; display: inline-block; - width: 98%; + width: 100%; } -#page-mod-margic-view .annotationtype { - margin-top: 5px; - border-bottom: 1px solid #dbdbdb; - padding-bottom: 5px; +#page-mod-margic-view .margic-btn-round-small { + width: 1.6rem; + height: 1.6rem; + border-radius: 50%; + padding: 0; + margin-right: 5px; + margin-bottom: 5px; } \ No newline at end of file diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index b463aba..ee7d65d 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -55,6 +55,7 @@ {{#annotationmode}} {{#str}}hideannotations, mod_margic{{/str}} {{/annotationmode}} {{/entries.0}} {{#canmanageentries}} {{#str}}annotationssummary, mod_margic{{/str}} {{/canmanageentries}} + {{#entries.0}} @@ -153,38 +154,32 @@

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}}
-
- - {{{userpicturestr}}} - - - {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - +
+ + {{type}} +
-
- - {{type}} - {{#canmanageentries}} - {{#defaulttype}} - (S) - {{/defaulttype}} - {{^defaulttype}} - (M) - {{/defaulttype}} - {{/canmanageentries}} +
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + {{#canbeedited}} + + {{/canbeedited}}
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} -
- - {{#text}}{{text}}{{/text}} - {{^text}}-{{/text}} - - {{#canbeedited}} - - {{/canbeedited}}
{{/annotations}} From 8771592dc602dfb76c6930fc7004a71dd7c3129e Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 19 Jul 2022 16:31:06 +0200 Subject: [PATCH 04/60] fix (annotations): Fix for correct display order of annotations (not fully working yet) --- locallib.php | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/locallib.php b/locallib.php index f0f2f3c..52c5d65 100644 --- a/locallib.php +++ b/locallib.php @@ -73,6 +73,8 @@ class margic { /** @var array Array of error messages encountered during the execution of margic related operations. */ private $errors = array(); + /** @var array Temp helper array with entry nodes sorted by occurance */ + private $nodepositions = array(); /** * Constructor for the base margic class. * @@ -248,6 +250,29 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $strmanager = get_string_manager(); + // Custom sort function for annotations. + function sortannotation($a, $b) { + // var_dump($a); + // var_dump($b); + + if (!isset($a->position)) { + var_dump('Fehler: keine Position an Element A'); + var_dump($a->id); + return true; + } else if (!isset($b->position)) { + var_dump('Fehler: keine Position an Element B'); + var_dump($b->id); + return false; + } + + + if ($a->position === $b->position) { + return $a->startposition > $b->startposition; + } + + return $a->position > $b->position; + } + foreach ($this->entries as $i => $entry) { $this->entries[$i]->user = $DB->get_record('user', array('id' => $entry->userid)); @@ -268,6 +293,28 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $this->entries[$i]->entrycanbeedited = false; } + + // Index entry for annotation sorting. + $position = 0; + + $doc = new DOMDocument(); + $doc->loadHTML($this->entries[$i]->text); + + $this->index_original($doc); + + // var_dump('NEW ENTRY'); + // var_dump($i); + + // var_dump('
'); + // var_dump('
'); + + // var_dump('NEW nodepositions'); + // var_dump($this->nodepositions); + + // var_dump('
'); + // var_dump('
'); + + // Get annotations for entry. $this->entries[$i]->annotations = array_values($DB->get_records('margic_annotations', array('margic' => $this->cm->instance, 'entry' => $entry->id))); foreach ($this->entries[$i]->annotations as $key => $annotation) { @@ -292,8 +339,44 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { } else { $this->entries[$i]->annotations[$key]->canbeedited = false; } + + + // Get position of startcontainer. + $xpath = new DOMXpath($doc); + $nodelist = $xpath->query('/' . $annotation->startcontainer); + + echo('$annotation->id
'); + var_dump($annotation->id); + echo "
"; + + echo('$annotation->startcontainer
'); + var_dump($annotation->startcontainer); + echo "
"; + + + // var_dump('$nodelist'); + // var_dump($nodelist); + + // var_dump('$nodepositions'); + // var_dump($this->nodepositions); + + foreach ($this->nodepositions as $position => $node) { + if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array. + $this->entries[$i]->annotations[$key]->position = $position; // If so asssign its position to annotation. + echo "POSITION OF ANNOTATION:
"; + echo $this->entries[$i]->annotations[$key]->position; + echo "
"; + break; + } + } } + // Sort annotations by position and offset of startcontainer. + usort($this->entries[$i]->annotations, "sortannotation"); + + // Reset nodepositions with empty array for next entry. + $this->nodepositions = array(); + } else { unset($this->entries[$i]); } @@ -515,4 +598,37 @@ public function get_pagecountoptions() { public function get_sortmode() { return $this->sortmode; } + + private function index_original($doc) { + + // var_dump('index_original: $doc'); + // var_dump($doc); + + foreach ($doc->childNodes as $childnode) { + // var_dump('index_original: $childnode'); + // var_dump($childnode); + + $this->search_dom_node($childnode); + } + } + + private function search_dom_node(DOMNode $domnode, &$position = 0) { + // var_dump('search_dom_node: $domnode'); + // var_dump($domnode); + + // var_dump('search_dom_node: $nodepositions'); + // var_dump($nodepositions); + + // var_dump('search_dom_node: $position'); + // var_dump($position); + + foreach ($domnode->childNodes as $node) { + $this->nodepositions[$position] = $node; + $position = $position + 1; + + if ($node->hasChildNodes()) { + $this->search_dom_node($node, $position); + } + } + } } From e63f536fb26fffc90949bbe54122a39e53f21a73 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 19 Jul 2022 18:39:34 +0200 Subject: [PATCH 05/60] fix (edittime): timeopen and timeclose are now used everywhere as intended; removed days feature for weekly course mode --- classes/local/results.php | 14 ++++++------- classes/output/margic_view.php | 18 +++++++++++++++-- db/install.xml | 1 - edit.php | 37 +++++++++------------------------- lang/de/margic.php | 11 +++++----- lang/en/margic.php | 9 ++++----- mod_form.php | 20 ------------------ templates/margic_view.mustache | 9 ++++++--- view.php | 36 ++++++++++++++------------------- 9 files changed, 62 insertions(+), 93 deletions(-) diff --git a/classes/local/results.php b/classes/local/results.php index fd19598..17f3959 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -17,8 +17,6 @@ /** * Results utilities for margic. * - * 2020071700 Moved these functions from lib.php to here. - * * @package mod_margic * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later @@ -27,6 +25,7 @@ define('MARGIC_EVENT_TYPE_OPEN', 'open'); define('MARGIC_EVENT_TYPE_CLOSE', 'close'); + use mod_margic\local\results; use stdClass; use csv_export_writer; @@ -35,7 +34,7 @@ use calendar_event; /** - * Utility class for margic results. + * Utility class for mod_margic. * * @package mod_margic * @copyright 2022 coactum GmbH @@ -64,6 +63,7 @@ public static function margic_update_calendar(stdClass $margic, $cmid) { // Margic start calendar events. $event = new stdClass(); $event->eventtype = MARGIC_EVENT_TYPE_OPEN; + // The MOOTYPER_EVENT_TYPE_OPEN event should only be an action event if no close time is specified. $event->type = empty($margic->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; @@ -155,16 +155,16 @@ public static function margic_update_calendar(stdClass $margic, $cmid) { } /** - * Returns availability status. - * Added 20200903. + * Returns if margic is available and entries are editable. * * @param var $margic */ - /* public static function margic_available($margic) { + public static function margic_available($margic) { $timeopen = $margic->timeopen; $timeclose = $margic->timeclose; + return (($timeopen == 0 || time() >= $timeopen) && ($timeclose == 0 || time() < $timeclose)); - } */ + } /** * Download entries in this margic activity. diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 5386a10..8409de9 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -63,8 +63,14 @@ class margic_view implements renderable, templatable { /** @var bool */ protected $caneditentries; /** @var int */ + protected $edittimestarts; + /** @var bool */ + protected $edittimenotstarted; + /** @var int */ protected $edittimeends; /** @var bool */ + protected $edittimehasended; + /** @var bool */ protected $canmanageentries; /** @var string */ protected $sesskey; @@ -100,6 +106,8 @@ class margic_view implements renderable, templatable { * @param int $entryareawidth Width of the entry area * @param int $annotationareawidth Width of the annotation area * @param bool $caneditentries If own entries can be edited + * @param int $edittimestarts Time when entries can be edited + * @param bool $edittimenotstarted If edit time has not started * @param int $edittimeends Time when entries cant be edited anymore * @param bool $edittimehasended If edit time has ended * @param bool $canmanageentries If entries can be managed @@ -115,8 +123,10 @@ class margic_view implements renderable, templatable { * @param bool $canmakeannotations If user can make annotations * @param array $annotationtypes Array with annotation types for form */ - public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, $caneditentries, $edittimeends, $edittimehasended, $canmanageentries, - $sesskey, $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, $canmakeannotations, $annotationtypes) { + public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, + $caneditentries, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, $sesskey, + $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, + $canmakeannotations, $annotationtypes) { $this->cm = $cm; $this->cmid = $this->cm->id; @@ -129,6 +139,8 @@ public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $this->annotationareawidth = $annotationareawidth; $this->entryareawidth = 100 - $annotationareawidth; $this->caneditentries = $caneditentries; + $this->edittimestarts = $edittimestarts; + $this->edittimenotstarted = $edittimenotstarted; $this->edittimeends = $edittimeends; $this->edittimehasended = $edittimehasended; $this->canmanageentries = $canmanageentries; @@ -203,6 +215,8 @@ public function export_for_template(renderer_base $output) { $data->entryareawidth = $this->entryareawidth; $data->annotationareawidth = $this->annotationareawidth; $data->caneditentries = $this->caneditentries; + $data->edittimestarts = $this->edittimestarts; + $data->edittimenotstarted = $this->edittimenotstarted; $data->edittimeends = $this->edittimeends; $data->edittimehasended = $this->edittimehasended; $data->canmanageentries = $this->canmanageentries; diff --git a/db/install.xml b/db/install.xml index 0932fac..aac7537 100644 --- a/db/install.xml +++ b/db/install.xml @@ -13,7 +13,6 @@ - diff --git a/edit.php b/edit.php index 269c334..ca3d517 100644 --- a/edit.php +++ b/edit.php @@ -66,28 +66,8 @@ require_capability('mod/margic:addentries', $context); -// Prevent creating and editing of entries when activity is closed. -$timenow = time(); -if ($course->format == 'weeks' and $moduleinstance->days) { - $timestart = $course->startdate + (($coursesections->section - 1) * 604800); - if ($moduleinstance->days) { - $timefinish = $timestart + (3600 * 24 * $moduleinstance->days); - } else { - $timefinish = $course->enddate; - } -} else if (! ((($moduleinstance->timeopen == 0 || time() >= $moduleinstance->timeopen) - && ($moduleinstance->timeclose == 0 || time() < $moduleinstance->timeclose)))) { // If margic is not available? - // If used, set calendar availability time limits on the margics. - $timestart = $moduleinstance->timeopen; - $timefinish = $moduleinstance->timeclose; - $moduleinstance->days = 0; -} else { - // Have no time limits on the margics. - $timestart = false; - $timefinish = false; -} - -if (($entryid && !$moduleinstance->editall) || ($timefinish && (time() >= $timefinish))) { +// Prevent creating and editing of entries if user is not allowed to edit entry or activity is not available. +if (($entryid && !$moduleinstance->editall) || !results::margic_available($moduleinstance)) { // Trigger invalid_access_attempt with redirect to the view page. $params = array( 'objectid' => $id, @@ -158,11 +138,6 @@ } else if ($fromform = $form->get_data()) { $timenow = time(); - // Prevent creation dates in the future. - if ($moduleinstance->editdates && $fromform->timecreated > $timenow) { - redirect('view.php?id='.$id, get_string('entrydateinfuture', 'margic'), null, notification::NOTIFY_ERROR); - } - // Relink using the proper entryid because draft area didn't have an itemid associated when creating new entry. $newentry = new stdClass(); $newentry->margic = $moduleinstance->id; @@ -216,7 +191,13 @@ $event->add_record_snapshot('margic', $moduleinstance); $event->trigger(); - redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id)); + if ($moduleinstance->editdates && $fromform->timecreated > $timenow) { + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryadded', 'mod_margic') . + ' ' . get_string('editdateinfuture', 'mod_margic'), null, notification::NOTIFY_WARNING); + } else { + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + } + } echo $OUTPUT->header(); diff --git a/lang/de/margic.php b/lang/de/margic.php index f1f3429..6ea110b 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -29,7 +29,6 @@ $string['alias'] = 'Schlagwort'; $string['aliases'] = 'Schlagwörter'; $string['aliases_help'] = 'Jedem Margic-Eintrag kann eine Liste an Schlagwörtern oder Aliasnamen zugeordnet werden. Verwenden Sie für jedes Schlagwort eine neue Zeile (nicht getrennt durch Kommata)'; -$string['alwaysopen'] = 'Immer geöffnet'; $string['attachment'] = 'Anhang'; $string['attachment_help'] = 'Sie können auch Dateien an einen Margic-Eintrag anhängen.'; $string['blankentry'] = 'Leerer Eintrag'; @@ -39,16 +38,14 @@ $string['created'] = 'Erstellt vor {$a->days} Tagen und {$a->hours} Stunden.'; $string['csvexport'] = 'Exportieren nach .csv'; $string['dateformat'] = 'Standard-Datumsformat'; -$string['daysavailable'] = 'Verfügbare Tage'; -$string['daysavailable_help'] = 'Wenn Sie das Wochenformat verwenden, können Sie einstellen, wie viele Tage die Margic-Instanz für die Verwendung geöffnet ist.'; $string['deadline'] = 'Offene Tage'; $string['details'] = 'Details'; $string['margic:addentries'] = 'Margic-Einträge hinzufügen'; $string['margic:addinstance'] = 'Margic-Instanzen hinzufügen'; $string['margic:manageentries'] = 'Margic-Einträge verwalten'; $string['margic:rate'] = 'Margic-Einträge bewerten'; -$string['margicclosetime'] = 'Endzeit'; -$string['margicclosetime_help'] = 'Wenn diese Option aktiviert ist, können Sie ein Datum festlegen, an dem die Margic-Instanz geschlossen wird und nicht mehr verwendet werden kann.'; +$string['margicclosetime'] = 'Endzeitpunkt'; +$string['margicclosetime_help'] = 'Wenn diese Option aktiviert ist können Sie ein Datum festlegen, an dem die Margic-Instanz geschlossen wird. Teilnehmende können danach keine Einträge mehr anlegen oder bearbeiten.'; $string['margicdescription'] = 'Beschreibung der Margic-Instanz'; $string['margicentrydate'] = 'Datum für diesen Eintrag festlegen'; $string['margicmail'] = 'Hallo {$a->user}, @@ -69,6 +66,7 @@ $string['editall_help'] = 'Wenn aktiviert, können Nutzer/innen alle eigenen Einträge in einem Margic bearbeiten.'; $string['editdates'] = 'Eintragsdatum bearbeiten'; $string['editdates_help'] = 'Wenn aktiviert, können Nutzer/innen das Datum jedes Eintrags bearbeiten.'; +$string['editingends'] = 'Bearbeitungszeitraum beginnt am {$a}'; $string['editingended'] = 'Die Bearbeitungszeit endete am {$a}'; $string['editingends'] = 'Bearbeitungszeitraum endet am {$a}'; $string['editthisentry'] = 'Diesen Eintrag bearbeiten'; @@ -176,7 +174,7 @@ $string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; $string['startoreditentry'] = 'Eintrag anlegen oder bearbeiten'; $string['editentrynotpossible'] = 'Bearbeiten des Eintrages nicht möglich.'; -$string['entrydateinfuture'] = 'Das Datum der Erstellung des Eintrages kann nicht in der Zukunft liegen.'; +$string['editdateinfuture'] = 'Das angegebene Erstelldatum des Eintrags liegt in der Zukunft.'; $string['currenttooldest'] = 'Zeige die Einträge vom Aktuellsten zum Ältesten'; $string['oldesttocurrent'] = 'Zeige die Einträge vom Ältesten zum Aktuellsten'; $string['lowestgradetohighest'] = 'Zeige die Einträge vom am niedrigsten Bewerteten zum Höchsten'; @@ -241,6 +239,7 @@ $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; $string['toggleannotation'] = 'Annotation aus- / einklappen'; $string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; +$string['entryadded'] = 'Eintrag angelegt oder bearbeitet.'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 603f82c..f3b2b26 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -35,7 +35,6 @@ $string['eventinvalidentryattempt'] = 'margic invalid entry attempt'; $string['accessdenied'] = 'Access denied'; -$string['alwaysopen'] = 'Always open'; $string['alias'] = 'Keyword'; $string['aliases'] = 'Keyword(s)'; $string['aliases_help'] = 'Each margic entry can have an associated list of keywords (or aliases). @@ -50,13 +49,11 @@ $string['configdateformat'] = 'This defines how dates are shown in margic reports. The default value, "M d, Y G:i" is Month, day, year and 24 hour format time. Refer to Date in the PHP manual for more examples and predefined date constants.'; $string['created'] = 'Created {$a->days} days and {$a->hours} hours ago.'; $string['csvexport'] = 'Export to .csv'; -$string['daysavailable'] = 'Days available'; -$string['daysavailable_help'] = 'If using Weekly format, you can set how many days the margic is open for use.'; $string['deadline'] = 'Days Open'; $string['dateformat'] = 'Default date format'; $string['details'] = 'Details'; $string['margicclosetime'] = 'Close time'; -$string['margicclosetime_help'] = 'If enabled, you can set a date for the margic to be closed and no longer open for use.'; +$string['margicclosetime_help'] = 'If this option is activated, you can set a date on which the Margic is closed. Participants will no longer be able to create or edit entries after that date.'; $string['margicentrydate'] = 'Set date for this entry'; $string['margicopentime'] = 'Open time'; $string['margicopentime_help'] = 'If enabled, you can set a date for the margic to be opened for use.'; @@ -64,6 +61,7 @@ $string['editall_help'] = 'When enabled, users can edit all own entries in the margic.'; $string['editdates'] = 'Edit entry dates'; $string['editdates_help'] = 'When enabled, users can edit the date of any entry.'; +$string['editingstarts'] = 'Editing period starts at {$a}'; $string['editingended'] = 'Editing period has ended at {$a}'; $string['editingends'] = 'Editing period ends at {$a}'; $string['editthisentry'] = 'Edit this entry'; @@ -189,7 +187,7 @@ $string['viewallmargics'] = 'View all margics in course'; $string['startoreditentry'] = 'Add or edit entry'; $string['editentrynotpossible'] = 'You can not edit this entry.'; -$string['entrydateinfuture'] = 'Entry date can not be in the future.'; +$string['editdateinfuture'] = 'The specified entry date is in the future.'; $string['currenttooldest'] = 'Show entries from current to oldest'; $string['oldesttocurrent'] = 'Show entries from oldest to current'; $string['lowestgradetohighest'] = 'Show entries from the lowest rated to the highest one'; @@ -254,6 +252,7 @@ $string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; $string['toggleannotation'] = 'Toggle annotation'; $string['toggleallannotations'] = 'Toggle all annotations'; +$string['entryadded'] = 'Entry added or modified.'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/mod_form.php b/mod_form.php index 6d8b040..671b851 100644 --- a/mod_form.php +++ b/mod_form.php @@ -57,26 +57,6 @@ public function definition() { // Add the header for availability. $mform->addElement('header', 'availibilityhdr', get_string('availability')); - // 20200915 Moved check so daysavailable is hidden unless using weekly format. - if ($COURSE->format == 'weeks') { - $options = array(); - $options[0] = get_string('alwaysopen', 'margic'); - for ($i = 1; $i <= 13; $i ++) { - $options[$i] = get_string('numdays', '', $i); - } - for ($i = 2; $i <= 16; $i ++) { - $days = $i * 7; - $options[$days] = get_string('numweeks', '', $i); - } - $options[365] = get_string('numweeks', '', 52); - $mform->addElement('select', 'days', get_string('daysavailable', 'margic'), $options); - $mform->addHelpButton('days', 'daysavailable', 'margic'); - - $mform->setDefault('days', '7'); - } else { - $mform->setDefault('days', '0'); - } - $mform->addElement('date_time_selector', 'timeopen', get_string('margicopentime', 'margic'), array( 'optional' => true, 'step' => 1 diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index ee7d65d..6127e53 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -25,6 +25,7 @@ {{/js}} {{#edittimehasended}}{{#edittimeends}}{{/edittimeends}}{{/edittimehasended}} +{{#edittimenotstarted}}{{#edittimestarts}}{{/edittimestarts}}{{/edittimenotstarted}} {{#canmanageentries}}

{{#str}}entries, mod_margic{{/str}}

{{/canmanageentries}} {{^canmanageentries}}

{{#str}}myentries, mod_margic{{/str}}

{{/canmanageentries}} @@ -44,7 +45,7 @@
- {{^edittimehasended}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimehasended}} + {{^edittimehasended}}{{^edittimenotstarted}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimenotstarted}}{{/edittimehasended}} {{#entries.0}} {{#canmanageentries}}{{/canmanageentries}} {{#canmanageentries}}{{#singleuser}} {{#str}}viewallentries, mod_margic{{/str}} {{/singleuser}}{{/canmanageentries}} @@ -52,10 +53,12 @@ {{#canmakeannotations}} {{#str}}viewandmakeannotations, mod_margic{{/str}} {{/canmakeannotations}} {{^canmakeannotations}} {{#str}}viewannotations, mod_margic{{/str}} {{/canmakeannotations}} {{/annotationmode}} - {{#annotationmode}} {{#str}}hideannotations, mod_margic{{/str}} {{/annotationmode}} + {{#annotationmode}} + {{#str}}hideannotations, mod_margic{{/str}} + + {{/annotationmode}} {{/entries.0}} {{#canmanageentries}} {{#str}}annotationssummary, mod_margic{{/str}} {{/canmanageentries}} - {{#entries.0}} diff --git a/view.php b/view.php index 841bac6..dbc9db2 100644 --- a/view.php +++ b/view.php @@ -147,26 +147,6 @@ echo $OUTPUT->box(format_module_intro('margic', $moduleinstance, $cm->id), 'generalbox mod_introbox', 'newmoduleintro'); } -// Set start and finish time. Needs to be reworked/simplified? -if ($course->format == 'weeks' and $moduleinstance->days) { - $timestart = $course->startdate + (($coursesections->section - 1) * 604800); - if ($moduleinstance->days) { - $timefinish = $timestart + (3600 * 24 * $moduleinstance->days); - } else { - $timefinish = $course->enddate; - } -} else if (! ((($moduleinstance->timeopen == 0 || time() >= $moduleinstance->timeopen) - && ($moduleinstance->timeclose == 0 || time() < $moduleinstance->timeclose)))) { // If margic is not available? - // If used, set calendar availability time limits on the margics. - $timestart = $moduleinstance->timeopen; - $timefinish = $moduleinstance->timeclose; - $moduleinstance->days = 0; -} else { - // Have no time limits on the margics. - $timestart = false; - $timefinish = false; -} - // Get grading of current user when margic is rated. if ($moduleinstance->assessed != 0) { $ratingaggregationmode = results::get_margic_aggregation($moduleinstance->assessed) . ' ' . get_string('forallmyentries', 'mod_margic'); @@ -178,6 +158,19 @@ $currentuserrating = false; } +// Calculate if edit time has started. +if (!$moduleinstance->timeopen) { + $edittimenotstarted = false; + $edittimestarts = false; +} else if ($moduleinstance->timeopen && $timenow >= $moduleinstance->timeopen) { + $edittimenotstarted = false; + $edittimestarts = $moduleinstance->timeopen; +} else if ($moduleinstance->timeopen && $timenow < $moduleinstance->timeopen) { + $edittimenotstarted = true; + $edittimestarts = $moduleinstance->timeopen; +} + +// Calculate if edit time has ended. $timenow = time(); if (!$moduleinstance->timeclose) { $edittimehasended = false; @@ -190,6 +183,7 @@ $edittimeends = $moduleinstance->timeclose; } +// Get width of annotation area. if (isset($moduleinstance->annotationareawidth)) { $annotationareawidth = $moduleinstance->annotationareawidth; } else { @@ -202,7 +196,7 @@ // Output page. $page = new margic_view($cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), $margic->get_sortmode(), get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $annotationareawidth, - $moduleinstance->editall, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, + $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), $annotationmode, $canmakeannotations, $margic->get_annotationtypes_for_form()); From 3ff330637cf1fcfb10872067bcfc5db1bb07db20 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 20 Jul 2022 19:24:42 +0200 Subject: [PATCH 06/60] feat (backup): working on backup, restore and course reset --- .../backup_margic_activity_task.class.php | 46 ++--- backup/moodle2/backup_margic_stepslib.php | 116 ++++++------- .../restore_margic_activity_task.class.php | 90 +++++----- backup/moodle2/restore_margic_stepslib.php | 161 +++++++++++------- classes/local/results.php | 4 +- classes/search/activity.php | 3 +- edit.php | 2 +- grade_entry.php | 2 +- lang/de/margic.php | 14 +- lang/en/margic.php | 12 +- lib.php | 157 ++++++++++++++--- locallib.php | 26 +-- view.php | 2 +- 13 files changed, 390 insertions(+), 245 deletions(-) diff --git a/backup/moodle2/backup_margic_activity_task.class.php b/backup/moodle2/backup_margic_activity_task.class.php index 4bb2731..7742b27 100644 --- a/backup/moodle2/backup_margic_activity_task.class.php +++ b/backup/moodle2/backup_margic_activity_task.class.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Defines backup_margic_activity_task class. + * The task that provides all the steps to perform a complete backup is defined here. * * @package mod_margic * @category backup @@ -28,50 +28,54 @@ require_once($CFG->dirroot.'/mod/margic/backup/moodle2/backup_margic_stepslib.php'); /** - * Provides the steps to perform one complete backup of the margic instance. + * The class provides all the settings and steps to perform one complete backup of mod_margic. */ class backup_margic_activity_task extends backup_activity_task { /** - * No specific settings for this activity. + * Defines particular settings for the plugin. */ protected function define_my_settings() { + // No particular settings for this activity. } /** - * Defines a backup step to store the instance data in the margic.xml file. + * Defines particular steps for the backup process. */ protected function define_my_steps() { $this->add_step(new backup_margic_activity_structure_step('margic_structure', 'margic.xml')); } /** - * Encodes URLs to the index.php and view.php scripts. + * Codes the transformations to perform in the activity in order to get transportable (encoded) links. * - * @param string $content some HTML text that eventually contains URLs to the activity instance scripts. - * @return string $content The content with the URLs encoded. + * @param string $content content. + * @return string $content content. */ public static function encode_content_links($content) { - global $CFG; - $base = preg_quote($CFG->wwwroot.'/mod/margic', '#'); + $base = preg_quote($CFG->wwwroot, "/"); + + // Link to the list of margics. + $search = "/(".$base."\/mod\/margic\/index.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@MARGICINDEX*$2@$', $content); - // Link to the list of diaries. - $pattern = "#(".$base."\/index.php\?id\=)([0-9]+)#"; - $content = preg_replace($pattern, '$@margicINDEX*$2@$', $content); + // Link to margic view by moduleid with optional userid if only entries of one user should be shown. + $search = "/(".$base."\/mod\/margic\/view.php\?id\=)([0-9]+)(&|&)userid=([0-9]+)/"; + $content = preg_replace($search, '$@MARGICVIEWBYID*$2*$4@$', $content); - // Link to margic view by moduleid. - $pattern = "#(".$base."\/view.php\?id\=)([0-9]+)#"; - $content = preg_replace($pattern, '$@margicVIEWBYID*$2@$', $content); + // Link to the edit page with optional entryid of entry that should be edited. + $search = "/(".$base."\/mod\/margic\/edit.php\?id\=)([0-9]+)(&|&)entryid=([0-9]+)/"; + $content = preg_replace($search, '$@MARGICEDITVIEW*$2*$4@$', $content); - // Link to margic report by moduleid. - $pattern = "#(".$base."\/report.php\?id\=)([0-9]+)#"; - $content = preg_replace($pattern, '$@margicREPORT*$2@$', $content); + // Link to the annotation summary by moduleid. + $search = "/(".$base."\/mod\/margic\/annotations_summary.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@MARGICANNOTATIONSUMMARY*$2@$', $content); - // Link to margic entry by moduleid. - $pattern = "#(".$base."\/edit.php\?id\=)([0-9]+)#"; - $content = preg_replace($pattern, '$@margicEDIT*$2@$', $content); + // Link to the page for editing annotation types with optional id of tyoe that should be edited. + $search = "/(".$base."\/mod\/margic\/annotation_types.php\?id\=)([0-9]+)(&|&)edit=([0-9]+)/"; + $content = preg_replace($search, '$@MARGICANNOTATIONTYPES*$2*$4@$', $content); return $content; } diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php index 9c06da0..a10911a 100644 --- a/backup/moodle2/backup_margic_stepslib.php +++ b/backup/moodle2/backup_margic_stepslib.php @@ -15,90 +15,80 @@ // along with Moodle. If not, see . /** - * Define all the backup steps that will be used by the backup_margic_activity_task + * Backup steps for mod_margic are defined here. * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_margic + * @category backup + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** - * Define the complete margic structure for backup, with file and id annotations. - * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Define the complete structure for backup, with file and id annotations. */ class backup_margic_activity_structure_step extends backup_activity_structure_step { /** - * Define the complete data structure for backup, with file and id annotations + * Defines the structure of the resulting xml file. * - * @return void + * @return backup_nested_element The structure wrapped by the common 'activity' element. */ protected function define_structure() { - - // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); - // Define each element separated. - $margic = new backup_nested_element('margic', array('id'), - array('name', - 'intro', - 'introformat', - 'days', - 'scale', - 'assessed', - 'assesstimestart', - 'assesstimefinish', - 'timemodified', - 'timeopen', - 'timeclose', - 'editall')); + // Replace with the attributes and final elements that the element will handle. + $margic = new backup_nested_element('margic', array('id'), array( + 'name', 'intro', 'introformat', 'timecreated', 'timemodified', + 'scale', 'assessed', 'assesstimestart', 'assesstimefinish', + 'timeopen', 'timeclose', 'editall', 'editdates', 'annotationareawidth')); $entries = new backup_nested_element('entries'); - $entry = new backup_nested_element('entry', array('id'), - array('userid', - 'timecreated', - 'timemodified', - 'text', - 'format', - 'rating', - 'entrycomment', - 'teacher', - 'timemarked', - 'mailed')); - - $tags = new backup_nested_element('entriestags'); - $tag = new backup_nested_element('tag', array('id'), - array('itemid', - 'rawname')); + + $entry = new backup_nested_element('entry', array('id'), array( + 'userid', 'timecreated', 'timemodified', 'text', 'format', + 'rating', 'entrycomment', 'formatcomment', 'teacher', + 'timemarked', 'mailed')); + + $annotations = new backup_nested_element('annotations'); + + $annotation = new backup_nested_element('annotation', array('id'), array( + 'userid', 'timecreated', 'timemodified', 'type', 'startcontainer', 'endcontainer', + 'startposition', 'endposition', 'text')); + + $tags = new backup_nested_element('tags'); + $tag = new backup_nested_element('tag', array('id'), array('itemid', 'rawname')); $ratings = new backup_nested_element('ratings'); - $rating = new backup_nested_element('rating', array('id'), - array('component', - 'ratingarea', - 'scaleid', - 'value', - 'userid', - 'timecreated', - 'timemodified')); - - // Build the tree. + $rating = new backup_nested_element('rating', array('id'), array( + 'component', 'ratingarea', 'scaleid', 'value', 'userid', + 'timecreated', 'timemodified')); + + // Build the tree with these elements with $margic as the root of the backup tree. $margic->add_child($entries); $entries->add_child($entry); + + $entry->add_child($annotations); + $annotations->add_child($annotation); + $entry->add_child($ratings); $ratings->add_child($rating); + $margic->add_child($tags); $tags->add_child($tag); - // Define sources. + // Define the source tables for the elements. + $margic->set_source_table('margic', array('id' => backup::VAR_ACTIVITYID)); - // All the rest of elements only happen if we are including user info. - if ($this->get_setting_value('userinfo')) { + if ($userinfo) { + + // Entries. $entry->set_source_table('margic_entries', array('margic' => backup::VAR_PARENTID)); + // Annotations. + $annotation->set_source_table('margic_annotations', array('entry' => backup::VAR_PARENTID)); + + // Ratings (core). $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, 'itemid' => backup::VAR_PARENTID, 'component' => backup_helper::is_sqlparam('mod_margic'), @@ -106,6 +96,7 @@ protected function define_structure() { $rating->set_source_alias('rating', 'value'); + // Tags (core). if (core_tag_tag::is_enabled('mod_margic', 'margic_entries')) { $tag->set_source_sql('SELECT t.id, ti.itemid, t.rawname FROM {tag} t @@ -118,19 +109,24 @@ protected function define_structure() { backup_helper::is_sqlparam('mod_margic'), backup::VAR_CONTEXTID)); } + } // Define id annotations. $margic->annotate_ids('scale', 'scale'); - $entry->annotate_ids('user', 'userid'); - $entry->annotate_ids('user', 'teacher'); $rating->annotate_ids('scale', 'scaleid'); $rating->annotate_ids('user', 'userid'); + if ($userinfo) { + $entry->annotate_ids('user', 'userid'); + $entry->annotate_ids('user', 'teacher'); + $annotation->annotate_ids('user', 'userid'); + } + // Define file annotations. - $margic->annotate_files('mod_margic', 'intro', null); // This file areas haven't itemid. + $margic->annotate_files('mod_margic', 'intro', null); // This file area has no itemid. $entry->annotate_files('mod_margic_entries', 'entry', 'id'); - $entry->annotate_files('mod_margic_entries', 'attachment', 'id'); + $entry->annotate_files('mod_margic_entries', 'feedback', 'id'); return $this->prepare_activity_structure($margic); } diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php index 08ff037..3297cd7 100644 --- a/backup/moodle2/restore_margic_activity_task.class.php +++ b/backup/moodle2/restore_margic_activity_task.class.php @@ -15,99 +15,88 @@ // along with Moodle. If not, see . /** - * Define all the backup steps that will be used by the backup_margic_activity_task + * The task that provides a complete restore of mod_margic is defined here. * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_margic + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot . '/mod/margic/backup/moodle2/restore_margic_stepslib.php'); +require_once($CFG->dirroot.'/mod/margic/backup/moodle2/restore_margic_stepslib.php'); /** - * margic restore task that provides all the settings and steps to perform one complete restore of the activity. - * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Restore task for mod_margic. */ class restore_margic_activity_task extends restore_activity_task { /** - * Define (add) particular settings this activity can have. + * Defines particular settings that this activity can have. */ protected function define_my_settings() { - // No particular settings for this activity. + return; } /** - * Define (add) particular steps this activity can have. + * Defines particular steps that this activity can have. + * + * @return base_step. */ protected function define_my_steps() { $this->add_step(new restore_margic_activity_structure_step('margic_structure', 'margic.xml')); } /** - * Define the contents in the activity that must be - * processed by the link decoder. + * Defines the contents in the activity that must be processed by the link decoder. * - * @return array + * @return array. */ public static function define_decode_contents() { $contents = array(); - $contents[] = new restore_decode_content('margic', array( - 'intro' - ), 'margic'); - $contents[] = new restore_decode_content('margic_entries', array( - 'text', - 'entrycomment' - ), 'margic_entry'); + + // Define the contents (files in textareas). + $contents[] = new restore_decode_content('margic', array('intro'), 'margic'); + $contents[] = new restore_decode_content('margic_entries', array('text', 'feedback'), 'margic_entry'); return $contents; } /** - * Define the decoding rules for links belonging - * to the activity to be executed by the link decoder. + * Defines the decoding rules for links belonging to the activity to be executed by the link decoder. * - * @return array of restore_decode_rule + * @return array. */ public static function define_decode_rules() { $rules = array(); - // List of margic's in the course. - $rules[] = new restore_decode_rule('margicINDEX', '/mod/margic/index.php?id=$1', 'course'); - // margic views by cm->id. - $rules[] = new restore_decode_rule('margicVIEWBYID', '/mod/margic/view.php?id=$1', 'course_module'); - // margic reports by cm->id. - $rules[] = new restore_decode_rule('margicREPORT', '/mod/margic/report.php?id=$1', 'course_module'); - // margic user edits by cm->id. - $rules[] = new restore_decode_rule('margicEDIT', '/mod/margic/edit.php?id=$1', 'course_module'); + + // Define the rules. + + $rules[] = new restore_decode_rule('MARGICINDEX', '/mod/margic/index.php?id=$1', 'course'); + $rules[] = new restore_decode_rule('MARGICVIEWBYID', '/mod/margic/view.php?id=$1&userid=$2', array('course_module', 'userid')); + $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1&entryid=$2', array('course_module', 'entryid')); + $rules[] = new restore_decode_rule('MARGICANNOTATIONSUMMARY', '/mod/margic/annotations_summary.php?id=$1', 'course_module'); + $rules[] = new restore_decode_rule('MARGICANNOTATIONTYPES', '/mod/margic/annotation_types.php?id=$1&edit=$2', array('course_module', 'edit')); return $rules; } /** - * Added fix from https://tracker.moodle.org/browse/MDL-34172 - */ - - /** - * Define the restore log rules that will be applied - * by the restore_logs_processor when restoring - * margic logs. - * It must return one array - * of restore_log_rule objects. + * Defines the restore log rules that will be applied by the + * restore_logs_processor when restoring mod_margic logs. It + * must return one array of restore_log_rule objects. * - * @return array of restore_log_rule + * @return array. */ public static function define_restore_log_rules() { $rules = array(); + // Define the rules. $rules[] = new restore_log_rule('margic', 'view', 'view.php?id={course_module}', '{margic}'); - $rules[] = new restore_log_rule('margic', 'view responses', 'report.php?id={course_module}', '{margic}'); + $rules[] = new restore_log_rule('margic', 'view responses', 'view.php?id={course_module}', '{margic}'); $rules[] = new restore_log_rule('margic', 'add entry', 'edit.php?id={course_module}', '{margic}'); $rules[] = new restore_log_rule('margic', 'update entry', 'edit.php?id={course_module}', '{margic}'); - $rules[] = new restore_log_rule('margic', 'update feedback', 'report.php?id={course_module}', '{margic}'); + $rules[] = new restore_log_rule('margic', 'update feedback', 'view.php?id={course_module}', '{margic}'); return $rules; } @@ -115,15 +104,12 @@ public static function define_restore_log_rules() { /** * Define the restore log rules that will be applied * by the restore_logs_processor when restoring - * course logs. - * It must return one array - * of restore_log_rule objects. + * course logs. It must return one array + * of restore_log_rule objects * * Note this rules are applied when restoring course logs * by the restore final task, but are defined here at - * activity level. All them are rules not linked to any module instance (cmid = 0). - * - * @return array + * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { $rules = array(); diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php index 903fbc6..29fd9af 100644 --- a/backup/moodle2/restore_margic_stepslib.php +++ b/backup/moodle2/restore_margic_stepslib.php @@ -15,30 +15,29 @@ // along with Moodle. If not, see . /** - * Define all the restore steps that will be used by the restore_margic_activity_task + * All the steps to restore mod_margic are defined here. * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package mod_margic + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -use mod_margic\local\results; /** - * Define the complete margic structure for restore, with file and id annotations. - * - * @package mod_margic - * @copyright 2022 coactum GmbH - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * Defines the structure step to restore one mod_margic activity. */ class restore_margic_activity_structure_step extends restore_activity_structure_step { + /** @var newmargicid Store id of new margic. */ + protected $newmargicid = false; + /** - * Define the structure of the restore workflow. + * Defines the structure to be restored. * - * @return restore_path_element $structure + * @return restore_path_element[]. */ protected function define_structure() { $paths = array(); + $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('margic', '/activity/margic'); @@ -46,81 +45,113 @@ protected function define_structure() { if ($userinfo) { $paths[] = new restore_path_element('margic_entry', '/activity/margic/entries/entry'); $paths[] = new restore_path_element('margic_entry_rating', '/activity/margic/entries/entry/ratings/rating'); - $paths[] = new restore_path_element('margic_entry_tag', '/activity/margic/entriestags/tag'); + $paths[] = new restore_path_element('margic_entry_annotation', '/activity/margic/entries/entry/annotations/annotation'); + $paths[] = new restore_path_element('margic_entry_tag', '/activity/margic/tags/tag'); } - // Return the paths wrapped into standard activity structure. return $this->prepare_activity_structure($paths); } /** - * Process a margic restore. + * Restore margic. * - * @param object $margic - * The margic in object form - * @return void + * @param object $data data. */ - protected function process_margic($margic) { + protected function process_margic($data) { global $DB; - $margic = (object) $margic; - $oldid = $margic->id; - $margic->course = $this->get_courseid(); + $userinfo = $this->get_setting_value('userinfo'); - unset($margic->id); + $data = (object) $data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + + error_log('process_margic'); // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. - $margic->course = $this->get_courseid(); - $margic->assesstimestart = $this->apply_date_offset($margic->assesstimestart); - $margic->assesstimefinish = $this->apply_date_offset($margic->assesstimefinish); - $margic->timemodified = $this->apply_date_offset($margic->timemodified); - $margic->timeopen = $this->apply_date_offset($margic->timeopen); - $margic->timeclose = $this->apply_date_offset($margic->timeclose); - - if ($margic->scale < 0) { // Scale found, get mapping. - $margic->scale = - ($this->get_mappingid('scale', abs($margic->scale))); + if (!isset($data->assesstimestart)) { + $data->assesstimestart = 0; + } + $data->assesstimestart = $this->apply_date_offset($data->assesstimestart); + + if (!isset($data->assesstimefinish)) { + $data->assesstimefinish = 0; + } + $data->assesstimefinish = $this->apply_date_offset($data->assesstimefinish); + + if (!isset($data->timeopen)) { + $data->timeopen = 0; } + $data->timeopen = $this->apply_date_offset($data->timeopen); - // Insert the data record. - $newid = $DB->insert_record('margic', $margic); - $this->apply_activity_instance($newid); + if (!isset($data->timeclose)) { + $data->timeclose = 0; + } + $data->timeclose = $this->apply_date_offset($data->timeclose); + + if ($data->scale < 0) { // Scale found, get mapping. + $data->scale = - ($this->get_mappingid('scale', abs($data->scale))); + } + + $newitemid = $DB->insert_record('margic', $data); + $this->apply_activity_instance($newitemid); + $this->newmargicid = $newitemid; } /** - * Process a margicentry restore. + * Restore margic entry. * - * @param object $margicentry - * The margicentry in object form. - * @return void + * @param object $data data. + */ + protected function process_margic_entry($data) { + global $DB; + + error_log('process_margic_entry'); + + $data = (object) $data; + $oldid = $data->id; + + $data->margic = $this->get_new_parentid('margic'); + $data->userid = $this->get_mappingid('user', $data->userid); + + $newitemid = $DB->insert_record('margic_entries', $data); + $this->set_mapping('margic_entry', $oldid, $newitemid); + } + + /** + * Add annotations to restored margic entries. + * + * @param stdClass $data Tag */ - protected function process_margic_entry($margicentry) { + protected function process_margic_entry_annotation($data) { global $DB; - $margicentry = (object) $margicentry; + error_log('process_margic_entry_annotation'); - $oldid = $margicentry->id; - unset($margicentry->id); + $data = (object) $data; + + $oldid = $data->id; - $margicentry->margic = $this->get_new_parentid('margic'); - $margicentry->timemcreated = $this->apply_date_offset($margicentry->timecreated); - $margicentry->timemodified = $this->apply_date_offset($margicentry->timemodified); - $margicentry->timemarked = $this->apply_date_offset($margicentry->timemarked); - $margicentry->userid = $this->get_mappingid('user', $margicentry->userid); + $data->margic = $this->newmargicid; + $data->entry = $this->get_new_parentid('margic_entry'); + $data->userid = $this->get_mappingid('user', $data->userid); - $newid = $DB->insert_record('margic_entries', $margicentry); - $this->set_mapping('margic_entry', $oldid, $newid); + $newitemid = $DB->insert_record('margic_annotations', $data); + $this->set_mapping('margic_annotation', $oldid, $newitemid); } /** - * Add tags to restored entries. + * Add tags to restored margic entries. * - * @param stdClass $data - * Tag + * @param stdClass $data Tag */ protected function process_margic_entry_tag($data) { $data = (object) $data; + error_log('process_margic_entry_tag'); + + if (! core_tag_tag::is_enabled('mod_margic', 'margic_entries')) { // Tags disabled in server, nothing to process. return; } @@ -138,13 +169,14 @@ protected function process_margic_entry_tag($data) { /** * Process margic entries to provide a rating restore. * - * @param object $data - * The data in object form. + * @param object $data The data in object form. * @return void */ protected function process_margic_entry_rating($data) { global $DB; + error_log('process_margic_entry_rating'); + $data = (object) $data; // Cannot use ratings API, cause, it's missing the ability to specify times (modified/created). @@ -168,13 +200,24 @@ protected function process_margic_entry_rating($data) { } /** - * Once the database tables have been fully restored, restore the files - * - * @return void + * Defines post-execution actions like restoring files. */ protected function after_execute() { + error_log('margic restore after_execute BEGIN'); + + // Add margic related files, no need to match by itemname (just internally handled context). $this->add_related_files('mod_margic', 'intro', null); + + error_log('margic restore after_execute AFTERINTRO'); + $this->add_related_files('mod_margic_entries', 'text', null); - $this->add_related_files('mod_margic_entries', 'entrycomment', null); + + error_log('margic restore after_execute AFTERTEXT'); + + $this->add_related_files('mod_margic_entries', 'feedback', null); + + error_log('margic restore after_execute AFTERFEEDBACK'); + + } } diff --git a/classes/local/results.php b/classes/local/results.php index 17f3959..c93d656 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -356,7 +356,7 @@ public static function margic_get_editor_and_attachment_options($course, $contex // If maxfiles would be set to an int and more files are given the editor saves them all but saves the overcouting incorrect so that white box is diaplayed. - // For a file attachements field (not really needed here?). + // For a file attachments field (not really needed here?). $attachmentoptions = array( 'subdirs' => false, 'maxfiles' => 1, @@ -494,7 +494,7 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $editoroptions['autosave'] = false; $data = file_prepare_standard_editor($data, 'feedback_' . $entry->id, $editoroptions, $context, 'mod_margic', 'feedback', $data->entry); - $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); + // $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); $data->{'rating_' . $entry->id} = $entry->rating; diff --git a/classes/search/activity.php b/classes/search/activity.php index 26b6f96..2eb42d7 100644 --- a/classes/search/activity.php +++ b/classes/search/activity.php @@ -50,7 +50,8 @@ public function uses_file_indexing() { public function get_search_fileareas() { $fileareas = array( 'intro', - MARGIC_INTROATTACHMENT_FILEAREA + 'entry', + 'feedback' ); // Fileareas. return $fileareas; } diff --git a/edit.php b/edit.php index ca3d517..d67aae6 100644 --- a/edit.php +++ b/edit.php @@ -125,7 +125,7 @@ list ($editoroptions, $attachmentoptions) = results::margic_get_editor_and_attachment_options($course, $context, $moduleinstance); $data = file_prepare_standard_editor($data, 'text', $editoroptions, $context, 'mod_margic', 'entry', $data->entryid); -$data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entryid); +// $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entryid); // Create form. $form = new mod_margic_entry_form(null, array('margic' => $moduleinstance->editdates, 'editoroptions' => $editoroptions)); diff --git a/grade_entry.php b/grade_entry.php index 7cfa6fb..6978e7d 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -79,7 +79,7 @@ list ($editoroptions, $attachmentoptions) = results::margic_get_editor_and_attachment_options($course, $context, $moduleinstance); $data = file_prepare_standard_editor($data, 'feedback_' . $entry->id, $editoroptions, $context, 'mod_margic', 'feedback', $data->entry); -$data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); +// $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); $data->{'rating_' . $entry->id} = $entry->rating; diff --git a/lang/de/margic.php b/lang/de/margic.php index 6ea110b..0d8baab 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -29,8 +29,6 @@ $string['alias'] = 'Schlagwort'; $string['aliases'] = 'Schlagwörter'; $string['aliases_help'] = 'Jedem Margic-Eintrag kann eine Liste an Schlagwörtern oder Aliasnamen zugeordnet werden. Verwenden Sie für jedes Schlagwort eine neue Zeile (nicht getrennt durch Kommata)'; -$string['attachment'] = 'Anhang'; -$string['attachment_help'] = 'Sie können auch Dateien an einen Margic-Eintrag anhängen.'; $string['blankentry'] = 'Leerer Eintrag'; $string['calendarend'] = '{$a} schließt'; $string['calendarstart'] = '{$a} öffnet'; @@ -118,10 +116,6 @@ $string['rating'] = 'Bewertung'; $string['savedrating'] = 'Gespeicherte Bewertung für diesen Eintrag'; $string['newrating'] = 'Neue Bewertung für diesen Eintrag'; -$string['removeentries'] = 'Alle Einträge entfernen'; -$string['removemessages'] = 'Alle Margic-Einträge entfernen'; -$string['reportsingle'] = 'Alle Margic-Einträge dieser Person anzeigen.'; -$string['reportsingleallentries'] = 'Alle Margic-Einträge dieser Person.'; $string['returnto'] = 'Zurück zu {$a}'; $string['returntoreport'] = 'Zurück zur Übersicht von {$a}'; $string['savesettings'] = 'Einstellungen speichern'; @@ -239,7 +233,13 @@ $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; $string['toggleannotation'] = 'Annotation aus- / einklappen'; $string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; -$string['entryadded'] = 'Eintrag angelegt oder bearbeitet.'; +$string['entryadded'] = 'Eintrag angelegt oder bearbeitet'; +$string['deletealluserdata'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags löschen'; +$string['alluserdatadeleted'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags wurden entfernt'; +$string['deletealltags'] = 'Nur alle Tags löschen'; +$string['tagsdeleted'] = 'Alle Tags gelöscht'; +$string['deleteallratings'] = 'Nur alle Bewertungen löschen'; +$string['ratingsdeleted'] = 'Alle Bewertungen gelöscht'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index f3b2b26..d98c4fe 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -41,8 +41,6 @@ Enter each keyword on a new line (not separated by commas).'; $string['and'] = ' and '; -$string['attachment'] = 'Attachment'; -$string['attachment_help'] = 'You can optionally attach one or more files to a margic entry.'; $string['blankentry'] = 'Blank entry'; $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; @@ -133,10 +131,6 @@ $string['rating'] = 'Rating'; $string['savedrating'] = 'Rating saved for this entry'; $string['newrating'] = 'New rating for this entry'; -$string['removeentries'] = 'Remove all entries'; -$string['removemessages'] = 'Remove all margic entries'; -$string['reportsingle'] = 'Get all margic entries for this user.'; -$string['reportsingleallentries'] = 'All margic entries for this user.'; $string['returnto'] = 'Return to {$a}'; $string['returntoreport'] = 'Return to report page for - {$a}'; $string['savesettings'] = 'Save settings'; @@ -253,6 +247,12 @@ $string['toggleannotation'] = 'Toggle annotation'; $string['toggleallannotations'] = 'Toggle all annotations'; $string['entryadded'] = 'Entry added or modified.'; +$string['deletealluserdata'] = 'Delete all entries, annotations, files, ratings and tags'; +$string['alluserdatadeleted'] = 'All entries, annotations, files, ratings and tags are deleted'; +$string['deletealltags'] = 'Delete only all tags'; +$string['tagsdeleted'] = 'All tags deleted'; +$string['deleteallratings'] = 'Delete only all ratings'; +$string['ratingsdeleted'] = 'All ratings deleted'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/lib.php b/lib.php index c20c2b8..3925d03 100644 --- a/lib.php +++ b/lib.php @@ -550,7 +550,15 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { */ function margic_reset_course_form_definition(&$mform) { $mform->addElement('header', 'margicheader', get_string('modulenameplural', 'margic')); - $mform->addElement('advcheckbox', 'reset_margic', get_string('removemessages', 'margic')); + $mform->addElement('checkbox', 'reset_margic_all', get_string('deletealluserdata', 'margic')); + + $mform->addElement('checkbox', 'reset_margic_ratings', get_string('deleteallratings', 'margic')); + $mform->disabledIf('reset_margic_ratings', 'reset_margic_all', 'checked'); + $mform->setAdvanced('reset_margic_ratings'); + + $mform->addElement('checkbox', 'reset_margic_tags', get_string('deletealltags', 'margic')); + $mform->disabledIf('reset_margic_tags', 'reset_margic_all', 'checked'); + $mform->setAdvanced('reset_margic_tags'); } /** @@ -560,51 +568,158 @@ function margic_reset_course_form_definition(&$mform) { * @return array */ function margic_reset_course_form_defaults($course) { - return array('reset_margic' => 1); + return array('reset_margic_all' => 1, 'reset_margic_ratings' => 0, 'reset_margic_tags' => 0); } /** - * Actual implementation of the reset course functionality, delete all the - * data responses for course $data->courseid. + * This function is used by the reset_course_userdata function in moodlelib. + * This function will remove all userdata from the specified margic. * - * @param object $data the data submitted from the reset course. - * @return array status array + * @param object $data The data submitted from the reset course. + * @return array $status Status array. */ function margic_reset_userdata($data) { global $CFG, $DB; + require_once($CFG->libdir . '/filelib.php'); require_once($CFG->dirroot . '/rating/lib.php'); + $modulename = get_string('modulenameplural', 'margic'); $status = array(); - // THIS FUNCTION NEEDS REWRITE! - if (! empty($data->reset_margic)) { - - $sql = "SELECT d.id - FROM {margic} d - WHERE d.course = ?"; - $params = array( - $data->courseid - ); + // Get margics in course that should be resetted. + $sql = "SELECT m.id + FROM {margic} m + WHERE m.course = ?"; + + $params = array( + $data->courseid + ); + + $margics = $DB->get_records_sql($sql, $params); + + // Get ratings manager. + if (!empty($data->reset_margic_all) || !empty($data->reset_margic_ratings)) { + $rm = new rating_manager(); + $ratingdeloptions = new stdClass; + $ratingdeloptions->component = 'mod_margic'; + $ratingdeloptions->ratingarea = 'entry'; + } + + // Delete entries and their annotations, files, ratings and tags. + if (!empty($data->reset_margic_all)) { + + foreach ($margics as $margicid => $unused) { + if (!$cm = get_coursemodule_from_instance('margic', $margicid)) { + continue; + } + + // Remove files. + $context = context_module::instance($cm->id); + $fs->delete_area_files($context->id, 'mod_margic', 'entry'); + $fs->delete_area_files($context->id, 'mod_margic', 'feedback'); + + // Remove ratings. + $ratingdeloptions->contextid = $context->id; + $rm->delete_ratings($ratingdeloptions); + + // Remove tags. + core_tag_tag::delete_instances('mod_margic', null, $context->id); + } + + // Remove all grades from gradebook (if that is not already done by the reset_gradebook_grades). + if (empty($data->reset_gradebook_grades)) { + margic_reset_gradebook($data->courseid); + } + + // Delete the annotations of all entries. + $DB->delete_records_select('margic_annotations', "margic IN ($sql)", $params); + + // Delete all entries. $DB->delete_records_select('margic_entries', "margic IN ($sql)", $params); $status[] = array( - 'component' => get_string('modulenameplural', 'margic'), - 'item' => get_string('removeentries', 'margic'), + 'component' => $modulename, + 'item' => get_string('alluserdatadeleted', 'margic'), 'error' => false ); } + // Delete ratings only. + if (!empty($data->reset_margic_ratings) ) { + + if ($margics) { + foreach ($margics as $margicid => $unused) { + if (!$cm = get_coursemodule_from_instance('margic', $margicid)) { + continue; + } + + $context = context_module::instance($cm->id); + $ratingdeloptions->contextid = $context->id; + $rm->delete_ratings($ratingdeloptions); + } + } + + // Remove all grades from gradebook (if that is not already done by the reset_gradebook_grades). + if (empty($data->reset_gradebook_grades)) { + margic_reset_gradebook($data->courseid); + } + } + + // Delete tags only. + if (!empty($data->reset_margic_tags) ) { + if ($margics) { + foreach ($margics as $margicid => $unused) { + if (!$cm = get_coursemodule_from_instance('margic', $margicid)) { + continue; + } + + $context = context_module::instance($cm->id); + core_tag_tag::delete_instances('mod_margic', null, $context->id); + } + } + + $status[] = array('component' => $modulename, 'item' => get_string('tagsdeleted', 'margic'), 'error' => false); + } + + // Updating dates - shift may be negative too. + if ($data->timeshift) { + // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. + // See MDL-9367. + shift_course_mod_dates('margic', array('assesstimestart', 'assesstimefinish', 'timeopen', 'timeclose'), $data->timeshift, $data->courseid); + $status[] = array('component' => $modulename, 'item' => get_string('datechanged'), 'error' => false); + } + return $status; } +/** + * Removes all grades in the margic gradebook + * + * @global object + * @param int $courseid + */ +function margic_reset_gradebook($courseid) { + global $DB; + + $params = array($courseid); + + $sql = "SELECT ma.*, cm.idnumber as cmidnumber, ma.course as courseid + FROM {margic} ma, {course_modules} cm, {modules} m + WHERE m.name='margic' AND m.id=cm.module AND cm.instance=ma.id AND ma.course=?"; + + if ($margics = $DB->get_records_sql($sql, $params)) { + foreach ($margics as $margic) { + margic_grade_item_update($margic, 'reset'); + } + } +} + /** * Get margic grades for a user. * - * @param object $margic - * if is null, all margics - * @param int $userid - * if is false all users + * @param object $margic If null, all margics + * @param int $userid If false all users * @return object $grades */ function margic_get_user_grades($margic, $userid = 0) { diff --git a/locallib.php b/locallib.php index 52c5d65..662a98e 100644 --- a/locallib.php +++ b/locallib.php @@ -256,12 +256,12 @@ function sortannotation($a, $b) { // var_dump($b); if (!isset($a->position)) { - var_dump('Fehler: keine Position an Element A'); - var_dump($a->id); + // var_dump('Fehler: keine Position an Element A'); + // var_dump($a->id); return true; } else if (!isset($b->position)) { - var_dump('Fehler: keine Position an Element B'); - var_dump($b->id); + // var_dump('Fehler: keine Position an Element B'); + // var_dump($b->id); return false; } @@ -345,13 +345,13 @@ function sortannotation($a, $b) { $xpath = new DOMXpath($doc); $nodelist = $xpath->query('/' . $annotation->startcontainer); - echo('$annotation->id
'); - var_dump($annotation->id); - echo "
"; + // echo('$annotation->id
'); + // var_dump($annotation->id); + // echo "
"; - echo('$annotation->startcontainer
'); - var_dump($annotation->startcontainer); - echo "
"; + // echo('$annotation->startcontainer
'); + // var_dump($annotation->startcontainer); + // echo "
"; // var_dump('$nodelist'); @@ -363,9 +363,9 @@ function sortannotation($a, $b) { foreach ($this->nodepositions as $position => $node) { if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array. $this->entries[$i]->annotations[$key]->position = $position; // If so asssign its position to annotation. - echo "POSITION OF ANNOTATION:
"; - echo $this->entries[$i]->annotations[$key]->position; - echo "
"; + // echo "POSITION OF ANNOTATION:
"; + // echo $this->entries[$i]->annotations[$key]->position; + // echo "
"; break; } } diff --git a/view.php b/view.php index dbc9db2..9aeeb4b 100644 --- a/view.php +++ b/view.php @@ -159,6 +159,7 @@ } // Calculate if edit time has started. +$timenow = time(); if (!$moduleinstance->timeopen) { $edittimenotstarted = false; $edittimestarts = false; @@ -171,7 +172,6 @@ } // Calculate if edit time has ended. -$timenow = time(); if (!$moduleinstance->timeclose) { $edittimehasended = false; $edittimeends = false; From c2c21626dde9611cda54c3e045f323e126622a48 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 21 Jul 2022 12:30:18 +0200 Subject: [PATCH 07/60] fix (files): minor fix reverting changes --- edit.php | 2 +- grade_entry.php | 2 +- lib.php | 1 - version.php | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/edit.php b/edit.php index d67aae6..ca3d517 100644 --- a/edit.php +++ b/edit.php @@ -125,7 +125,7 @@ list ($editoroptions, $attachmentoptions) = results::margic_get_editor_and_attachment_options($course, $context, $moduleinstance); $data = file_prepare_standard_editor($data, 'text', $editoroptions, $context, 'mod_margic', 'entry', $data->entryid); -// $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entryid); +$data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entryid); // Create form. $form = new mod_margic_entry_form(null, array('margic' => $moduleinstance->editdates, 'editoroptions' => $editoroptions)); diff --git a/grade_entry.php b/grade_entry.php index 6978e7d..7cfa6fb 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -79,7 +79,7 @@ list ($editoroptions, $attachmentoptions) = results::margic_get_editor_and_attachment_options($course, $context, $moduleinstance); $data = file_prepare_standard_editor($data, 'feedback_' . $entry->id, $editoroptions, $context, 'mod_margic', 'feedback', $data->entry); -// $data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); +$data = file_prepare_standard_filemanager($data, 'attachment', $attachmentoptions, $context, 'mod_margic', 'attachment', $data->entry); $data->{'rating_' . $entry->id} = $entry->rating; diff --git a/lib.php b/lib.php index 3925d03..c304ef5 100644 --- a/lib.php +++ b/lib.php @@ -994,7 +994,6 @@ function margic_get_coursemodule($margicid) { /** * Serves the margic files. - * THIS FUNCTION MAY BE ORPHANED. APPEARS TO BE SO IN JOURNAL. * * @param stdClass $course Course object. * @param stdClass $cm Course module object. diff --git a/version.php b/version.php index 539bdba..1a54050 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'mod_margic'; $plugin->release = '1.1.3'; // User-friendly version number. -$plugin->version = 2022071801; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022072000; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; From 0011ed20066b23fb5a75a99c9d89ef9d8c55d28e Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 21 Jul 2022 13:16:43 +0200 Subject: [PATCH 08/60] fix (backup): small fix, files still not duplicated --- backup/moodle2/restore_margic_activity_task.class.php | 2 +- lib.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php index 3297cd7..768b16a 100644 --- a/backup/moodle2/restore_margic_activity_task.class.php +++ b/backup/moodle2/restore_margic_activity_task.class.php @@ -57,7 +57,7 @@ public static function define_decode_contents() { // Define the contents (files in textareas). $contents[] = new restore_decode_content('margic', array('intro'), 'margic'); - $contents[] = new restore_decode_content('margic_entries', array('text', 'feedback'), 'margic_entry'); + $contents[] = new restore_decode_content('margic_entries', array('text', 'entrycomment'), 'margic_entry'); return $contents; } diff --git a/lib.php b/lib.php index c304ef5..524565d 100644 --- a/lib.php +++ b/lib.php @@ -609,6 +609,8 @@ function margic_reset_userdata($data) { // Delete entries and their annotations, files, ratings and tags. if (!empty($data->reset_margic_all)) { + $fs = get_file_storage(); + foreach ($margics as $margicid => $unused) { if (!$cm = get_coursemodule_from_instance('margic', $margicid)) { continue; From d8e85b834a8e072b7bd1783959931c26ec584048 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 21 Jul 2022 15:53:55 +0200 Subject: [PATCH 09/60] feat (errortypes): renaming annotation_types table and adding errortype_templates table --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 4 +- annotations.php | 2 +- annotations_summary.php | 26 +++---- .../backup_margic_activity_task.class.php | 4 +- .../restore_margic_activity_task.class.php | 2 +- classes/output/margic_annotations_summary.php | 10 +-- classes/output/margic_view.php | 10 +-- classes/privacy/provider.php | 18 ++--- db/access.php | 2 +- db/install.php | 48 ++++-------- db/install.xml | 32 ++++++-- db/upgrade.php | 43 +++++++++++ annotation_types.php => errortypes.php | 76 +++++++++---------- ...tion_types_form.php => errortypes_form.php | 6 +- lang/de/margic.php | 32 ++++---- lang/en/margic.php | 32 ++++---- locallib.php | 38 +++++----- styles.css | 2 +- templates/margic_annotations_summary.mustache | 10 +-- templates/margic_view.mustache | 4 +- version.php | 2 +- view.php | 2 +- 24 files changed, 227 insertions(+), 182 deletions(-) rename annotation_types.php => errortypes.php (60%) rename annotation_types_form.php => errortypes_form.php (94%) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index acd301a..5d157da 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,2 +1,2 @@ -function _createForOfIteratorHelper(a){if("undefined"==typeof Symbol||null==a[Symbol.iterator]){if(Array.isArray(a)||(a=_unsupportedIterableToArray(a))){var b=0,c=function(){};return{s:c,n:function n(){if(b>=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c=a.comparePoint(b,0)&&0<=a.comparePoint(b,e)}catch(a){return!1}}function j(a){var b=a.nodeName.toLowerCase(),c=b;if("#text"===b){c="text()"}return c}function k(a){var b=0,c=a;while(c){if(c.nodeName===a.nodeName){b+=1}c=c.previousSibling}return b}function l(a){var b=j(a),c=k(a);return"".concat(b,"[").concat(c,"]")}function m(a,b){var c="",d=a;while(d!==b){if(!d){throw new Error("Node is not a descendant of root")}c=l(d)+"/"+c;d=d.parentNode}c="/"+c;c=c.replace(/\/$/,"");return c}function n(a,b,c){b=b.toUpperCase();for(var d=-1,e=0,f;ej){return null}}else{i=h;j=0}var m=n(e,i,j);if(!m){return null}e=m}}catch(a){f.e(a)}finally{f.f()}return e}function p(a){var b=1=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c=a.comparePoint(b,0)&&0<=a.comparePoint(b,e)}catch(a){return!1}}function j(a){var b=a.nodeName.toLowerCase(),c=b;if("#text"===b){c="text()"}return c}function k(a){var b=0,c=a;while(c){if(c.nodeName===a.nodeName){b+=1}c=c.previousSibling}return b}function l(a){var b=j(a),c=k(a);return"".concat(b,"[").concat(c,"]")}function m(a,b){var c="",d=a;while(d!==b){if(!d){throw new Error("Node is not a descendant of root")}c=l(d)+"/"+c;d=d.parentNode}c="/"+c;c=c.replace(/\/$/,"");return c}function n(a,b,c){b=b.toUpperCase();for(var d=-1,e=0,f;ej){return null}}else{i=h;j=0}var m=n(e,i,j);if(!m){return null}e=m}}catch(a){f.e(a)}finally{f.f()}return e}function p(a){var b=1.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .annotationtype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .annotationtype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"file":"annotations.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/annotations.js"],"names":["define","$","init","annotations","canmakeannotations","hide","removeClass","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","node","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","childNodes","comparePoint","e","getNodeName","nodeName","toLowerCase","result","getNodePosition","pos","tmp","previousSibling","getPathSegment","name","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","child","children","evaluateSimpleXPath","isSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","removeHighlights","pn","normalize","on","selectedrange","window","getSelection","getRangeAt","cloneContents","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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","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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"file":"annotations.min.js"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 68f89a4..d8c6d51 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -525,7 +525,7 @@ var id = this.id.replace('annotated-', ''); $('.annotationpreview-'+id).addClass('hovered'); $('.annotated-'+id).addClass('hovered'); - $('.annotation-box-' + id + ' .annotationtype').addClass('hovered'); + $('.annotation-box-' + id + ' .errortype').addClass('hovered'); }); @@ -533,7 +533,7 @@ var id = this.id.replace('annotated-', ''); $('.annotationpreview-'+id).removeClass('hovered'); $('.annotated-'+id).removeClass('hovered'); - $('.annotation-box-' + id + ' .annotationtype').removeClass('hovered'); + $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); }); // Highlight annotated text if annotationpreview is hovered diff --git a/annotations.php b/annotations.php index dd25cf9..276cdcb 100644 --- a/annotations.php +++ b/annotations.php @@ -94,7 +94,7 @@ require_once($CFG->dirroot . '/mod/margic/annotation_form.php'); // Instantiate form. -$mform = new annotation_form(null, array('types' => $margic->get_annotationtypes_for_form())); +$mform = new annotation_form(null, array('types' => $margic->get_errortypes_for_form())); if ($fromform = $mform->get_data()) { diff --git a/annotations_summary.php b/annotations_summary.php index c9c5e65..05c7356 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -70,17 +70,17 @@ // Delete annotation. if ($delete !== 0) { $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); - if ($DB->record_exists('margic_annotation_types', array('id' => $delete))) { + if ($DB->record_exists('margic_errortypes', array('id' => $delete))) { global $USER; - $at = $DB->get_record('margic_annotation_types', array('id' => $delete)); + $at = $DB->get_record('margic_errortypes', array('id' => $delete)); - if (($at->defaulttype == 1 && has_capability('mod/margic:editdefaultannotationtypes', $context)) + if (($at->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) || ($at->defaulttype == 0 && $at->userid == $USER->id)) { - $DB->delete_records('margic_annotation_types', array('id' => $delete)); - redirect($redirecturl, get_string('annotationtypedeleted', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + $DB->delete_records('margic_errortypes', array('id' => $delete)); + redirect($redirecturl, get_string('errortypedeleted', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); } @@ -110,12 +110,12 @@ } $participants = array_values(get_enrolled_users($context, 'mod/margic:addentries')); -$annotationtypes = $margic->get_annotationtypes_for_form(); +$errortypes = $margic->get_errortypes_for_form(); foreach ($participants as $key => $participant) { $participants[$key]->errors = array(); - foreach ($annotationtypes as $i => $type) { + foreach ($errortypes as $i => $type) { $sql = "SELECT COUNT(*) FROM {margic_annotations} a JOIN {margic_entries} e ON e.id = a.entry @@ -133,16 +133,16 @@ global $USER; -$allannotations = $margic->get_all_annotationtypes(); +$allannotations = $margic->get_all_errortypes(); -foreach ($annotationtypes as $i => $type) { +foreach ($errortypes as $i => $type) { $obj = new stdClass(); $obj->id = $allannotations[$i]->id; $obj->name = $type; $obj->color = $allannotations[$i]->color; $obj->defaulttype = $allannotations[$i]->defaulttype; - if ($obj->defaulttype == 1 && has_capability('mod/margic:editdefaultannotationtypes', $context)) { + if ($obj->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { $obj->canbeedited = true; } else if ($allannotations[$i]->userid == $USER->id) { $obj->canbeedited = true; @@ -150,13 +150,13 @@ $obj->canbeedited = false; } - $annotationtypes[$i] = $obj; + $errortypes[$i] = $obj; } -$annotationtypes = array_values($annotationtypes); +$errortypes = array_values($errortypes); // Output page. -$page = new margic_annotations_summary($cm->id, $participants, $annotationtypes); +$page = new margic_annotations_summary($cm->id, $participants, $errortypes); echo $OUTPUT->render($page); diff --git a/backup/moodle2/backup_margic_activity_task.class.php b/backup/moodle2/backup_margic_activity_task.class.php index 7742b27..8332bb0 100644 --- a/backup/moodle2/backup_margic_activity_task.class.php +++ b/backup/moodle2/backup_margic_activity_task.class.php @@ -74,8 +74,8 @@ public static function encode_content_links($content) { $content = preg_replace($search, '$@MARGICANNOTATIONSUMMARY*$2@$', $content); // Link to the page for editing annotation types with optional id of tyoe that should be edited. - $search = "/(".$base."\/mod\/margic\/annotation_types.php\?id\=)([0-9]+)(&|&)edit=([0-9]+)/"; - $content = preg_replace($search, '$@MARGICANNOTATIONTYPES*$2*$4@$', $content); + $search = "/(".$base."\/mod\/margic\/errortypes.php\?id\=)([0-9]+)(&|&)edit=([0-9]+)/"; + $content = preg_replace($search, '$@MARGICERRORTYPES*$2*$4@$', $content); return $content; } diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php index 768b16a..bec8fd8 100644 --- a/backup/moodle2/restore_margic_activity_task.class.php +++ b/backup/moodle2/restore_margic_activity_task.class.php @@ -76,7 +76,7 @@ public static function define_decode_rules() { $rules[] = new restore_decode_rule('MARGICVIEWBYID', '/mod/margic/view.php?id=$1&userid=$2', array('course_module', 'userid')); $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1&entryid=$2', array('course_module', 'entryid')); $rules[] = new restore_decode_rule('MARGICANNOTATIONSUMMARY', '/mod/margic/annotations_summary.php?id=$1', 'course_module'); - $rules[] = new restore_decode_rule('MARGICANNOTATIONTYPES', '/mod/margic/annotation_types.php?id=$1&edit=$2', array('course_module', 'edit')); + $rules[] = new restore_decode_rule('MARGICERRORTYPES', '/mod/margic/errortypes.php?id=$1&edit=$2', array('course_module', 'edit')); return $rules; } diff --git a/classes/output/margic_annotations_summary.php b/classes/output/margic_annotations_summary.php index 3175be3..5b10ea8 100644 --- a/classes/output/margic_annotations_summary.php +++ b/classes/output/margic_annotations_summary.php @@ -42,19 +42,19 @@ class margic_annotations_summary implements renderable, templatable { /** @var object */ protected $participants; /** @var object */ - protected $annotationtypes; + protected $errortypes; /** * Construct this renderable. * @param int $cmid The course module id * @param array $participants The participants of the margic instance - * @param array $annotationtypes The annotation types of the margic instance + * @param array $errortypes The annotation types of the margic instance */ - public function __construct($cmid, $participants, $annotationtypes) { + public function __construct($cmid, $participants, $errortypes) { $this->cmid = $cmid; $this->participants = $participants; - $this->annotationtypes = $annotationtypes; + $this->errortypes = $errortypes; } /** @@ -67,7 +67,7 @@ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->cmid = $this->cmid; $data->participants = $this->participants; - $data->annotationtypes = $this->annotationtypes; + $data->errortypes = $this->errortypes; return $data; } diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 8409de9..1dc6db0 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -93,7 +93,7 @@ class margic_view implements renderable, templatable { /** @var bool */ protected $canmakeannotations; /** @var object */ - protected $annotationtypes; + protected $errortypes; /** * Construct this renderable. * @param object $cm The course module @@ -121,12 +121,12 @@ class margic_view implements renderable, templatable { * @param int $entriescount The amount of all entries * @param bool $annotationmode If annotation mode is set * @param bool $canmakeannotations If user can make annotations - * @param array $annotationtypes Array with annotation types for form + * @param array $errortypes Array with annotation types for form */ public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, $caneditentries, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, $sesskey, $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, - $canmakeannotations, $annotationtypes) { + $canmakeannotations, $errortypes) { $this->cm = $cm; $this->cmid = $this->cm->id; @@ -154,7 +154,7 @@ public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $this->entriescount = $entriescount; $this->annotationmode = $annotationmode; $this->canmakeannotations = $canmakeannotations; - $this->annotationtypes = $annotationtypes; + $this->errortypes = $errortypes; } /** @@ -188,7 +188,7 @@ public function export_for_template(renderer_base $output) { // Add annotation form to entry. if ($this->annotationmode) { - $mform = new \annotation_form(new \moodle_url('/mod/margic/annotations.php', array('id' => $this->cmid)), array('types' => $this->annotationtypes)); + $mform = new \annotation_form(new \moodle_url('/mod/margic/annotations.php', array('id' => $this->cmid)), array('types' => $this->errortypes)); // Set default data. $mform->set_data(array('id' => $this->cmid, 'entry' => $entry->id)); diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index f599bce..5924669 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -80,14 +80,14 @@ public static function get_metadata(collection $items): collection { 'text' => 'privacy:metadata:margic_annotations:text', ], 'privacy:metadata:margic_annotations'); - // The table 'margic_annotation_types' stores the annotation types of all margics. - $items->add_database_table('margic_annotation_types', [ - 'userid' => 'privacy:metadata:margic_annotation_types:userid', - 'timecreated' => 'privacy:metadata:margic_annotation_types:timecreated', - 'timemodified' => 'privacy:metadata:margic_annotation_types:timemodified', - 'name' => 'privacy:metadata:margic_annotation_types:name', - 'color' => 'privacy:metadata:margic_annotation_types:color', - ], 'privacy:metadata:margic_annotation_types'); + // The table 'margic_errortypes' stores the annotation types of all margics. + $items->add_database_table('margic_errortypes', [ + 'userid' => 'privacy:metadata:margic_errortypes:userid', + 'timecreated' => 'privacy:metadata:margic_errortypes:timecreated', + 'timemodified' => 'privacy:metadata:margic_errortypes:timemodified', + 'name' => 'privacy:metadata:margic_errortypes:name', + 'color' => 'privacy:metadata:margic_errortypes:color', + ], 'privacy:metadata:margic_errortypes'); // The margic uses multiple subsystems that save personal data. $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); @@ -140,7 +140,7 @@ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist->add_from_sql($sql, $params); - // Annotationtypes have no specific contexts. + // TODO: Get errortypes for margic. return $contextlist; } diff --git a/db/access.php b/db/access.php index 98028e6..60353c7 100644 --- a/db/access.php +++ b/db/access.php @@ -93,7 +93,7 @@ ) ), - 'mod/margic:editdefaultannotationtypes' => array( + 'mod/margic:editdefaulterrortypes' => array( 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, diff --git a/db/install.php b/db/install.php index b1d899d..4834a15 100644 --- a/db/install.php +++ b/db/install.php @@ -32,107 +32,91 @@ function xmldb_margic_install() { $errortype = new stdClass(); $errortype->id = 1; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_verb'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 2; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_syntax'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 3; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_congruence'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 4; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_other'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 5; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'expression'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 6; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'orthography'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 7; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'punctuation'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); $errortype = new stdClass(); $errortype->id = 8; - $errortype->userid = 0; $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'other'; $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; - $errortype->unused = 0; - $errortype->replaces = null; + $errortype->userid = 0; - $DB->insert_record('margic_annotation_types', $errortype); + $DB->insert_record('margic_errortype_templates', $errortype); return true; } diff --git a/db/install.xml b/db/install.xml index aac7537..66be3e2 100644 --- a/db/install.xml +++ b/db/install.xml @@ -58,8 +58,8 @@ - - + + @@ -78,17 +78,35 @@
- +
+ + + + + - - + + + + + + + +
+ + + + + + + - - + + diff --git a/db/upgrade.php b/db/upgrade.php index bc3d32a..79c3b53 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -65,5 +65,48 @@ function xmldb_margic_upgrade($oldversion) { } + if ($oldversion < 2022072100) { + + $table = new xmldb_table('margic_errortype_templates'); + + // Adding fields to table margic_errortypes_templates. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('color', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('defaulttype', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table margic_errortypes_templates. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table margic_errortypes_templates. + $table->add_index('userid', XMLDB_INDEX_NOTUNIQUE, array('userid')); + + // Conditionally launch create table for margic_errortypes_templates. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Add the order field to the table margic_errortypes. + $table = new xmldb_table('margic_annotation_types'); + $field = new xmldb_field('order', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'userid'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + if ($dbman->table_exists($table)) { + $dbman->rename_table($table, 'margic_errortypes'); + } + + // Margic savepoint reached. + upgrade_mod_savepoint(true, 2022072100, 'margic'); + } + + + return true; } diff --git a/annotation_types.php b/errortypes.php similarity index 60% rename from annotation_types.php rename to errortypes.php index 8c1af69..d9dc59d 100644 --- a/annotation_types.php +++ b/errortypes.php @@ -28,7 +28,7 @@ require(__DIR__.'/../../config.php'); require_once(__DIR__.'/lib.php'); require_once($CFG->dirroot . '/mod/margic/locallib.php'); -require_once($CFG->dirroot . '/mod/margic/annotation_types_form.php'); +require_once($CFG->dirroot . '/mod/margic/errortypes_form.php'); // Course_module ID. $id = required_param('id', PARAM_INT); @@ -71,8 +71,8 @@ $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); if ($edit !== 0) { - $editedtype = $DB->get_record('margic_annotation_types', array('id' => $edit)); - if ($editedtype && (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && has_capability('mod/margic:editdefaultannotationtypes', $context)) + $editedtype = $DB->get_record('margic_errortypes', array('id' => $edit)); + if ($editedtype && (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) || (isset($editedtype->defaulttype) && isset($editedtype->userid) && $editedtype->defaulttype == 0 && $editedtype->userid == $USER->id)) { $editedtypeid = $edit; $editedtypename = $editedtype->name; @@ -82,7 +82,7 @@ } // Instantiate form. -$mform = new annotation_types_form(null, array('editdefaulttype' => has_capability('mod/margic:editdefaultannotationtypes', $context))); +$mform = new errortypes_form(null, array('editdefaulttype' => has_capability('mod/margic:editdefaulterrortypes', $context))); if (isset($editedtypeid)) { $mform->set_data(array('id' => $id, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor, 'defaulttype' => $editeddefaulttype)); @@ -96,54 +96,54 @@ // In this case you process validated data. $mform->get_data() returns data posted in form. if ($fromform->typeid == 0 && isset($fromform->typename)) { // Create new annotation type. - $annotationtype = new stdClass(); + $errortype = new stdClass(); - if ($fromform->defaulttype === 1 && has_capability('mod/margic:editdefaultannotationtypes', $context)) { - $annotationtype->userid = 0; - $annotationtype->defaulttype = 1; + if ($fromform->defaulttype === 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { + $errortype->userid = 0; + $errortype->defaulttype = 1; } else { - $annotationtype->userid = $USER->id; - $annotationtype->defaulttype = 0; + $errortype->userid = $USER->id; + $errortype->defaulttype = 0; } - $annotationtype->timecreated = time(); - $annotationtype->timemodified = 0; - $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); - $annotationtype->color = $fromform->color; - $annotationtype->unused = 0; - $annotationtype->replaces = null; + $errortype->timecreated = time(); + $errortype->timemodified = 0; + $errortype->name = format_text($fromform->typename, 1, array('para' => false)); + $errortype->color = $fromform->color; + $errortype->unused = 0; + $errortype->replaces = null; - $DB->insert_record('margic_annotation_types', $annotationtype); + $DB->insert_record('margic_errortypes', $errortype); - redirect($redirecturl, get_string('annotationtypeadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + redirect($redirecturl, get_string('errortypeadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else if ($fromform->typeid !== 0 && isset($fromform->typename)) { // Update existing annotation type. - $annotationtype = $DB->get_record('margic_annotation_types', array('id' => $fromform->typeid)); + $errortype = $DB->get_record('margic_errortypes', array('id' => $fromform->typeid)); - if ($annotationtype && (isset($annotationtype->defaulttype) && $annotationtype->defaulttype == 1 && has_capability('mod/margic:editdefaultannotationtypes', $context)) - || (isset($annotationtype->defaulttype) && isset($annotationtype->userid) && $annotationtype->defaulttype == 0 && $annotationtype->userid == $USER->id)) { - $annotationtype->timemodified = time(); - $annotationtype->name = format_text($fromform->typename, 1, array('para' => false)); - $annotationtype->color = $fromform->color; + if ($errortype && (isset($errortype->defaulttype) && $errortype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) + || (isset($errortype->defaulttype) && isset($errortype->userid) && $errortype->defaulttype == 0 && $errortype->userid == $USER->id)) { + $errortype->timemodified = time(); + $errortype->name = format_text($fromform->typename, 1, array('para' => false)); + $errortype->color = $fromform->color; - if (has_capability('mod/margic:editdefaultannotationtypes', $context)) { + if (has_capability('mod/margic:editdefaulterrortypes', $context)) { global $USER; - if ($fromform->defaulttype === 1 && $annotationtype->defaulttype !== $fromform->defaulttype) { - $annotationtype->defaulttype = 1; - $annotationtype->userid = 0; - } else if ($fromform->defaulttype === 0 && $annotationtype->defaulttype !== $fromform->defaulttype) { - $annotationtype->defaulttype = 0; - $annotationtype->userid = $USER->id; + if ($fromform->defaulttype === 1 && $errortype->defaulttype !== $fromform->defaulttype) { + $errortype->defaulttype = 1; + $errortype->userid = 0; + } else if ($fromform->defaulttype === 0 && $errortype->defaulttype !== $fromform->defaulttype) { + $errortype->defaulttype = 0; + $errortype->userid = $USER->id; } } - $DB->update_record('margic_annotation_types', $annotationtype); - redirect($redirecturl, get_string('annotationtypeedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + $DB->update_record('margic_errortypes', $errortype); + redirect($redirecturl, get_string('errortypeedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { - redirect($redirecturl, get_string('annotationtypecantbeedited', 'mod_margic'), null, notification::NOTIFY_ERROR); + redirect($redirecturl, get_string('errortypecantbeedited', 'mod_margic'), null, notification::NOTIFY_ERROR); } } else { - redirect($redirecturl, get_string('annotationtypeinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); + redirect($redirecturl, get_string('errortypeinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); } } @@ -152,12 +152,12 @@ 'context' => $context )); -$PAGE->set_url('/mod/margic/annotation_types.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/margic/errortypes.php', array('id' => $cm->id)); if (isset($editedtypeid)) { - $PAGE->navbar->add(get_string('editannotationtype', 'mod_margic')); + $PAGE->navbar->add(get_string('editerrortype', 'mod_margic')); } else { - $PAGE->navbar->add(get_string('addannotationtype', 'mod_margic')); + $PAGE->navbar->add(get_string('adderrortype', 'mod_margic')); } $PAGE->set_title(get_string('modulename', 'mod_margic').': ' . $margicname); diff --git a/annotation_types_form.php b/errortypes_form.php similarity index 94% rename from annotation_types_form.php rename to errortypes_form.php index b868026..a258166 100644 --- a/annotation_types_form.php +++ b/errortypes_form.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * File containing the class definition for the annotationtypes form for the margic. + * File containing the class definition for the errortypes form for the margic. * * @package mod_margic * @copyright 2022 coactum GmbH @@ -34,7 +34,7 @@ * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ -class annotation_types_form extends moodleform { +class errortypes_form extends moodleform { /** * Define the form - called by parent constructor @@ -51,7 +51,7 @@ public function definition() { $mform->addElement('hidden', 'typeid', null); $mform->setType('typeid', PARAM_INT); - $mform->addElement('text', 'typename', get_string('nameofannotationtype', 'mod_margic')); + $mform->addElement('text', 'typename', get_string('nameoferrortype', 'mod_margic')); $mform->setType('typename', PARAM_TEXT); $mform->addRule('typename', null, 'required', null, 'client'); diff --git a/lang/de/margic.php b/lang/de/margic.php index 0d8baab..5bbd405 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -194,15 +194,15 @@ $string['annotationssummary'] = 'Annotationsauswertung und Fehlertypen'; $string['participant'] = 'TeilnehmerIn'; $string['backtooverview'] = 'Zurück zur Übersicht'; -$string['addannotationtype'] = 'Fehlertypen anlegen'; -$string['annotationtypeadded'] = 'Fehlertyp angelegt'; -$string['editannotationtype'] = 'Fehlertyp bearbeiten'; -$string['annotationtypeedited'] = 'Fehlertyp bearbeitet'; -$string['annotationtypecantbeedited'] = 'Fehlertyp konnte nicht geändert werden'; -$string['deleteannotationtype'] = 'Fehlertyp entfernen'; -$string['annotationtypedeleted'] = 'Fehlertyp entfernt'; -$string['annotationtypeinvalid'] = 'Fehlertyp ungültig'; -$string['nameofannotationtype'] = 'Name des Fehlertyps'; +$string['adderrortype'] = 'Fehlertypen anlegen'; +$string['errortypeadded'] = 'Fehlertyp angelegt'; +$string['editerrortype'] = 'Fehlertyp bearbeiten'; +$string['errortypeedited'] = 'Fehlertyp bearbeitet'; +$string['errortypecantbeedited'] = 'Fehlertyp konnte nicht geändert werden'; +$string['deleteerrortype'] = 'Fehlertyp entfernen'; +$string['errortypedeleted'] = 'Fehlertyp entfernt'; +$string['errortypeinvalid'] = 'Fehlertyp ungültig'; +$string['nameoferrortype'] = 'Name des Fehlertyps'; $string['annotationcreated'] = 'Erstellt am {$a}'; $string['annotationmodified'] = 'Bearbeitet am {$a}'; $string['editannotation'] = 'Bearbeiten'; @@ -220,7 +220,7 @@ $string['annotatedtextnotfound'] = 'Annotierter Text nicht gefunden'; $string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist (z. B. durch eine nachträgliche Änderung des ursprünglichen Beitrags) ungültig geworden. Die Markierung für diese Annotierung muss deshalb neu gesetzt werden.'; $string['notallowedtodothis'] = 'Vorgang nicht möglich.'; -$string['deletedannotationtype'] = 'Gelöschter Typ'; +$string['deletederrortype'] = 'Gelöschter Typ'; $string['errtypedeleted'] = 'Fehlertyp nicht vorhanden.'; $string['grader'] = 'Bewerter'; $string['feedbackupdated'] = 'Rückmeldung und / oder Note aktualisiert'; @@ -244,7 +244,7 @@ // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; $string['privacy:metadata:margic_annotations'] = 'Enthält die in allen Margics gemacht Annotationen.'; -$string['privacy:metadata:margic_annotation_types'] = 'Enthält die Fehlertypen aus allen Margics.'; +$string['privacy:metadata:margic_errortypes'] = 'Enthält die Fehlertypen aus allen Margics.'; $string['privacy:metadata:margic_entries:margic'] = 'ID des Margic, zu dem der Eintrag gehört.'; $string['privacy:metadata:margic_entries:userid'] = 'ID des Benutzers, zu dem der Eintrag gehört.'; $string['privacy:metadata:margic_entries:timecreated'] = 'Datum, an dem der Eintrag erstellt wurde.'; @@ -261,11 +261,11 @@ $string['privacy:metadata:margic_annotations:timemodified'] = 'Zeitpunkt der letzten Änderung der Annotation.'; $string['privacy:metadata:margic_annotations:type'] = 'ID des Typs der Annotation.'; $string['privacy:metadata:margic_annotations:text'] = 'Inhalt der Annotation.'; -$string['privacy:metadata:margic_annotation_types:userid'] = 'ID des Benutzers, der den Fehlertyp erstellt hat.'; -$string['privacy:metadata:margic_annotation_types:timecreated'] = 'Datum, an dem der Fehlertyp erstellt wurde.'; -$string['privacy:metadata:margic_annotation_types:timemodified'] = 'Zeitpunkt der letzten Änderung des Fehlertyps.'; -$string['privacy:metadata:margic_annotation_types:name'] = 'Name des Fehlertyps.'; -$string['privacy:metadata:margic_annotation_types:color'] = 'Farbe des Fehlertyps als Hex-Wert.'; +$string['privacy:metadata:margic_errortypes:userid'] = 'ID des Benutzers, der den Fehlertyp erstellt hat.'; +$string['privacy:metadata:margic_errortypes:timecreated'] = 'Datum, an dem der Fehlertyp erstellt wurde.'; +$string['privacy:metadata:margic_errortypes:timemodified'] = 'Zeitpunkt der letzten Änderung des Fehlertyps.'; +$string['privacy:metadata:margic_errortypes:name'] = 'Name des Fehlertyps.'; +$string['privacy:metadata:margic_errortypes:color'] = 'Farbe des Fehlertyps als Hex-Wert.'; $string['privacy:metadata:core_rating'] = 'Die zu den Margic-Einträgen hinzugefügten Bewertungen werden unter Verwendung des core_rating-Systems gespeichert.'; $string['privacy:metadata:core_files'] = 'Dateien, die mit Margic-Einträgen verknüpft sind, werden mithilfe des Systems core_files gespeichert.'; $string['privacy:metadata:preference:sortoption'] = 'Die Präferenz für die Sortierung jedes Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index d98c4fe..f618305 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -207,15 +207,15 @@ $string['annotationssummary'] = 'Annotations summary and error types'; $string['participant'] = 'Participant'; $string['backtooverview'] = 'Back to overview'; -$string['addannotationtype'] = 'Add annotation type'; -$string['annotationtypeadded'] = 'Annotation type added'; -$string['editannotationtype'] = 'Edit annotation type'; -$string['annotationtypeedited'] = 'Annotation type edited'; -$string['annotationtypecantbeedited'] = 'Annotation type could not be changed'; -$string['deleteannotationtype'] = 'Delete annotation type'; -$string['annotationtypedeleted'] = 'Annotation type deleted'; -$string['annotationtypeinvalid'] = 'Annotation type invalid'; -$string['nameofannotationtype'] = 'Name of annotation type'; +$string['adderrortype'] = 'Add annotation type'; +$string['errortypeadded'] = 'Annotation type added'; +$string['editerrortype'] = 'Edit annotation type'; +$string['errortypeedited'] = 'Annotation type edited'; +$string['errortypecantbeedited'] = 'Annotation type could not be changed'; +$string['deleteerrortype'] = 'Delete annotation type'; +$string['errortypedeleted'] = 'Annotation type deleted'; +$string['errortypeinvalid'] = 'Annotation type invalid'; +$string['nameoferrortype'] = 'Name of annotation type'; $string['annotationcreated'] = 'Created at {$a}'; $string['annotationmodified'] = 'Modified at {$a}'; $string['editannotation'] = 'Edit'; @@ -233,7 +233,7 @@ $string['annotatedtextnotfound'] = 'Annotated text not found'; $string['annotatedtextinvalid'] = 'The originally annotated text has become invalid (e.g. due to a subsequent change to the original entry). The marking for this annotation must therefore be redone.'; $string['notallowedtodothis'] = 'No permissions to do this.'; -$string['deletedannotationtype'] = 'Deleted type'; +$string['deletederrortype'] = 'Deleted type'; $string['errtypedeleted'] = 'Annotation type does not exists.'; $string['grader'] = 'Grader'; $string['feedbackupdated'] = 'Feedback and / or rating updated'; @@ -257,7 +257,7 @@ // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; $string['privacy:metadata:margic_annotations'] = 'Contains the annotations made in all margics.'; -$string['privacy:metadata:margic_annotation_types'] = 'Contains the annotation types of all margics.'; +$string['privacy:metadata:margic_errortypes'] = 'Contains the annotation types of all margics.'; $string['privacy:metadata:margic_entries:margic'] = 'ID of the Margic the entry belongs to.'; $string['privacy:metadata:margic_entries:userid'] = 'ID of the user the entry belongs to.'; $string['privacy:metadata:margic_entries:timecreated'] = 'Date on which the entry was created.'; @@ -274,11 +274,11 @@ $string['privacy:metadata:margic_annotations:timemodified'] = 'Time the annotation was last modified.'; $string['privacy:metadata:margic_annotations:type'] = 'Id of the type of the annotation.'; $string['privacy:metadata:margic_annotations:text'] = 'Content of the annotation.'; -$string['privacy:metadata:margic_annotation_types:userid'] = 'ID of the user that made the annotation type.'; -$string['privacy:metadata:margic_annotation_types:timecreated'] = 'Date on which the annotation type was created.'; -$string['privacy:metadata:margic_annotation_types:timemodified'] = 'Time the annotation type was last modified.'; -$string['privacy:metadata:margic_annotation_types:name'] = 'Name of the annotation type.'; -$string['privacy:metadata:margic_annotation_types:color'] = 'Color of the annotation type as hex value.'; +$string['privacy:metadata:margic_errortypes:userid'] = 'ID of the user that made the annotation type.'; +$string['privacy:metadata:margic_errortypes:timecreated'] = 'Date on which the annotation type was created.'; +$string['privacy:metadata:margic_errortypes:timemodified'] = 'Time the annotation type was last modified.'; +$string['privacy:metadata:margic_errortypes:name'] = 'Name of the annotation type.'; +$string['privacy:metadata:margic_errortypes:color'] = 'Color of the annotation type as hex value.'; $string['privacy:metadata:core_rating'] = 'Ratings added to margic entries are stored using the core_rating system.'; $string['privacy:metadata:core_files'] = 'Files linked to margic entries are stored using the core_files system.'; $string['privacy:metadata:preference:sortoption'] = 'The preference for the sorting of each margic.'; diff --git a/locallib.php b/locallib.php index 662a98e..cb2b189 100644 --- a/locallib.php +++ b/locallib.php @@ -68,7 +68,7 @@ class margic { private $annotations = array(); /** @var array Array with all types of annotations */ - private $annotationtypes = array(); + private $errortypes = array(); /** @var array Array of error messages encountered during the execution of margic related operations. */ private $errors = array(); @@ -113,16 +113,16 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $select = "defaulttype = 1"; $select .= " OR userid = " . $USER->id; - $this->annotationtypes = (array) $DB->get_records_select('margic_annotation_types', $select); + $this->errortypes = (array) $DB->get_records_select('margic_errortypes', $select); foreach ($this->annotations as $key => $annotation) { - if (!array_key_exists($annotation->type, $this->annotationtypes) && $DB->record_exists('margic_annotation_types', array('id' => $annotation->type))) { - $this->annotationtypes[$annotation->type] = $DB->get_record('margic_annotation_types', array('id' => $annotation->type)); + if (!array_key_exists($annotation->type, $this->errortypes) && $DB->record_exists('margic_errortypes', array('id' => $annotation->type))) { + $this->errortypes[$annotation->type] = $DB->get_record('margic_errortypes', array('id' => $annotation->type)); } - if (isset($this->annotationtypes[$annotation->type])) { - $this->annotations[$key]->color = $this->annotationtypes[$annotation->type]->color; + if (isset($this->errortypes[$annotation->type])) { + $this->annotations[$key]->color = $this->errortypes[$annotation->type]->color; } } @@ -319,18 +319,18 @@ function sortannotation($a, $b) { foreach ($this->entries[$i]->annotations as $key => $annotation) { - if (!$DB->record_exists('margic_annotation_types', array('id' => $annotation->type))) { // If annotation type does not exist. + if (!$DB->record_exists('margic_errortypes', array('id' => $annotation->type))) { // If annotation type does not exist. $this->entries[$i]->annotations[$key]->color = 'FFFF00'; $this->entries[$i]->annotations[$key]->defaulttype = 0; - $this->entries[$i]->annotations[$key]->type = get_string('deletedannotationtype', 'mod_margic'); + $this->entries[$i]->annotations[$key]->type = get_string('deletederrortype', 'mod_margic'); } else { - $this->entries[$i]->annotations[$key]->color = $this->annotationtypes[$annotation->type]->color; - $this->entries[$i]->annotations[$key]->defaulttype = $this->annotationtypes[$annotation->type]->defaulttype; + $this->entries[$i]->annotations[$key]->color = $this->errortypes[$annotation->type]->color; + $this->entries[$i]->annotations[$key]->defaulttype = $this->errortypes[$annotation->type]->defaulttype; - if ($this->entries[$i]->annotations[$key]->defaulttype == 1 && $strmanager->string_exists($this->annotationtypes[$annotation->type]->name, 'mod_margic')) { - $this->entries[$i]->annotations[$key]->type = get_string($this->annotationtypes[$annotation->type]->name, 'mod_margic'); + if ($this->entries[$i]->annotations[$key]->defaulttype == 1 && $strmanager->string_exists($this->errortypes[$annotation->type]->name, 'mod_margic')) { + $this->entries[$i]->annotations[$key]->type = get_string($this->errortypes[$annotation->type]->name, 'mod_margic'); } else { - $this->entries[$i]->annotations[$key]->type = $this->annotationtypes[$annotation->type]->name; + $this->entries[$i]->annotations[$key]->type = $this->errortypes[$annotation->type]->name; } } @@ -470,23 +470,23 @@ public function get_annotations() { } /** - * Returns all annotationtypes. + * Returns all errortypes. * * @return array action */ - public function get_all_annotationtypes() { - return $this->annotationtypes; + public function get_all_errortypes() { + return $this->errortypes; } /** - * Returns annotationtype array for select form. + * Returns errortype array for select form. * * @return array action */ - public function get_annotationtypes_for_form() { + public function get_errortypes_for_form() { $types = array(); $strmanager = get_string_manager(); - foreach ($this->annotationtypes as $key => $type) { + foreach ($this->errortypes as $key => $type) { if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_margic')) { $types[$key] = get_string($type->name, 'mod_margic'); } else { diff --git a/styles.css b/styles.css index 687d136..8781221 100644 --- a/styles.css +++ b/styles.css @@ -147,7 +147,7 @@ #page-mod-margic-view .annotated:hover, #page-mod-margic-view .annotated_temp:hover, #page-mod-margic-view .hovered, -#page-mod-margic-view .annotationtypeheader .hovered { +#page-mod-margic-view .errortypeheader .hovered { background-color: lightblue !important; } diff --git a/templates/margic_annotations_summary.mustache b/templates/margic_annotations_summary.mustache index 1852baa..b2d1e13 100644 --- a/templates/margic_annotations_summary.mustache +++ b/templates/margic_annotations_summary.mustache @@ -25,25 +25,25 @@ {{/js}}
- {{#annotationtypes}} + {{#errortypes}} - {{/annotationtypes}} + {{/errortypes}} {{#participants}} diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 6127e53..24c1a52 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -157,9 +157,9 @@

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}}
-
+
- {{type}} + {{type}}
diff --git a/version.php b/version.php index 1a54050..1458b8d 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'mod_margic'; $plugin->release = '1.1.3'; // User-friendly version number. -$plugin->version = 2022072000; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022072100; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; diff --git a/view.php b/view.php index 9aeeb4b..a82af48 100644 --- a/view.php +++ b/view.php @@ -198,7 +198,7 @@ get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $annotationareawidth, $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), - $annotationmode, $canmakeannotations, $margic->get_annotationtypes_for_form()); + $annotationmode, $canmakeannotations, $margic->get_errortypes_for_form()); echo $OUTPUT->render($page); From 3ea9e0d6b6db0770b66f6d33a3de860effcdb4f4 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 21 Jul 2022 16:17:37 +0200 Subject: [PATCH 10/60] fix (errortypes): little temp fix, feature still in progress --- errortypes.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/errortypes.php b/errortypes.php index d9dc59d..8df5722 100644 --- a/errortypes.php +++ b/errortypes.php @@ -97,6 +97,10 @@ if ($fromform->typeid == 0 && isset($fromform->typename)) { // Create new annotation type. $errortype = new stdClass(); + $errortype->timecreated = time(); + $errortype->timemodified = 0; + $errortype->name = format_text($fromform->typename, 1, array('para' => false)); + $errortype->color = $fromform->color; if ($fromform->defaulttype === 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { $errortype->userid = 0; @@ -106,10 +110,7 @@ $errortype->defaulttype = 0; } - $errortype->timecreated = time(); - $errortype->timemodified = 0; - $errortype->name = format_text($fromform->typename, 1, array('para' => false)); - $errortype->color = $fromform->color; + $errortype->order = 0; // Temp. $errortype->unused = 0; $errortype->replaces = null; @@ -136,6 +137,11 @@ } } + $errortype->order = 0; // Temp. + $errortype->unused = 0; + $errortype->replaces = null; + + $DB->update_record('margic_errortypes', $errortype); redirect($redirecturl, get_string('errortypeedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { From 6fa479c98a69d6ff9c673a10dd0b062216743fb6 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Fri, 22 Jul 2022 15:51:26 +0200 Subject: [PATCH 11/60] feat (errortypes): working on feature --- annotations_summary.php | 105 ++++++++++++---- classes/output/margic_annotations_summary.php | 16 ++- db/install.xml | 4 +- db/upgrade.php | 4 +- errortypes.php | 93 +++++++++++---- errortypes_form.php | 17 ++- lang/de/margic.php | 25 +++- lang/en/margic.php | 43 ++++--- lib.php | 31 +++-- locallib.php | 26 ++-- mod_form.php | 29 ++++- templates/margic_annotations_summary.mustache | 112 ++++++++++++++---- 12 files changed, 380 insertions(+), 125 deletions(-) diff --git a/annotations_summary.php b/annotations_summary.php index 05c7356..86c3b74 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -38,6 +38,12 @@ // ID of type that should be deleted. $delete = optional_param('delete', 0, PARAM_INT); +// ID of type that should be deleted. +$addtomargic = optional_param('addtomargic', 0, PARAM_INT); + +// If template (1) or margic (2) error type. +$mode = optional_param('mode', null, PARAM_INT); + $margic = margic::get_margic_instance($id, $m, false, 'currententry', 0, 1); $moduleinstance = $margic->get_module_instance(); @@ -67,19 +73,52 @@ require_capability('mod/margic:makeannotations', $context); +if ($addtomargic) { + $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); + + if ($DB->record_exists('margic_errortype_templates', array('id' => $addtomargic))) { + + global $USER; + + $type = $DB->get_record('margic_errortype_templates', array('id' => $addtomargic)); + + if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { + $type->priority = count($margic->get_margic_errortypes()) + 1; + $type->margic = $moduleinstance->id; + + $DB->insert_record('margic_errortypes', $type); + + redirect($redirecturl, get_string('errortypeadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); + } + } else { + redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); + } +} + // Delete annotation. -if ($delete !== 0) { +if ($delete !== 0 && $mode) { + $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); - if ($DB->record_exists('margic_errortypes', array('id' => $delete))) { + + if ($mode == 1) { // If type is template error type. + $table = 'margic_errortype_templates'; + } else if ($mode == 2) { // If type is margic error type. + $table = 'margic_errortypes'; + } + + if ($DB->record_exists($table, array('id' => $delete))) { global $USER; - $at = $DB->get_record('margic_errortypes', array('id' => $delete)); + $type = $DB->get_record($table, array('id' => $delete)); - if (($at->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) - || ($at->defaulttype == 0 && $at->userid == $USER->id)) { + if ($mode == 2 || + ($type->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) + || ($type->defaulttype == 0 && $type->userid == $USER->id)) { - $DB->delete_records('margic_errortypes', array('id' => $delete)); + $DB->delete_records($table, array('id' => $delete)); redirect($redirecturl, get_string('errortypedeleted', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); @@ -131,32 +170,54 @@ $participants[$key]->errors = array_values($participants[$key]->errors); } -global $USER; +$margicerrortypes = $margic->get_margic_errortypes(); +$strmanager = get_string_manager(); -$allannotations = $margic->get_all_errortypes(); +foreach ($margicerrortypes as $i => $type) { + $margicerrortypes[$i]->canbeedited = true; -foreach ($errortypes as $i => $type) { - $obj = new stdClass(); - $obj->id = $allannotations[$i]->id; - $obj->name = $type; - $obj->color = $allannotations[$i]->color; - $obj->defaulttype = $allannotations[$i]->defaulttype; + if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_margic')) { + $margicerrortypes[$i]->name = get_string($type->name, 'mod_margic'); + } else { + $margicerrortypes[$i]->name = $type->name; + } +} - if ($obj->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { - $obj->canbeedited = true; - } else if ($allannotations[$i]->userid == $USER->id) { - $obj->canbeedited = true; +$margicerrortypes = array_values($margicerrortypes); + +global $USER; + +$errortypetemplates = $margic->get_all_errortype_templates(); +foreach ($errortypetemplates as $id => $templatetype) { + if ($templatetype->defaulttype == 1) { + $errortypetemplates[$id]->type = get_string('standard', 'mod_margic'); + + if (has_capability('mod/margic:editdefaulterrortypes', $context)) { + $errortypetemplates[$id]->canbeedited = true; + } else { + $errortypetemplates[$id]->canbeedited = false; + } } else { - $obj->canbeedited = false; + $errortypetemplates[$id]->type = get_string('custom', 'mod_margic'); + + if ($templatetype->userid === $USER->id) { + $errortypetemplates[$id]->canbeedited = true; + } else { + $errortypetemplates[$id]->canbeedited = false; + } } - $errortypes[$i] = $obj; + if ($templatetype->defaulttype == 1 && $strmanager->string_exists($templatetype->name, 'mod_margic')) { + $errortypetemplates[$id]->name = get_string($templatetype->name, 'mod_margic'); + } else { + $errortypetemplates[$id]->name = $templatetype->name; + } } -$errortypes = array_values($errortypes); +$errortypetemplates = array_values($errortypetemplates); // Output page. -$page = new margic_annotations_summary($cm->id, $participants, $errortypes); +$page = new margic_annotations_summary($cm->id, $participants, $margicerrortypes, $errortypetemplates); echo $OUTPUT->render($page); diff --git a/classes/output/margic_annotations_summary.php b/classes/output/margic_annotations_summary.php index 5b10ea8..e7ebf01 100644 --- a/classes/output/margic_annotations_summary.php +++ b/classes/output/margic_annotations_summary.php @@ -42,19 +42,22 @@ class margic_annotations_summary implements renderable, templatable { /** @var object */ protected $participants; /** @var object */ - protected $errortypes; - + protected $margicerrortypes; + /** @var object */ + protected $errortypetemplates; /** * Construct this renderable. * @param int $cmid The course module id * @param array $participants The participants of the margic instance - * @param array $errortypes The annotation types of the margic instance + * @param array $margicerrortypes The errortypes used in the margic instance + * @param array $errortypetemplates The errortype templates available for the current user */ - public function __construct($cmid, $participants, $errortypes) { + public function __construct($cmid, $participants, $margicerrortypes, $errortypetemplates) { $this->cmid = $cmid; $this->participants = $participants; - $this->errortypes = $errortypes; + $this->margicerrortypes = $margicerrortypes; + $this->errortypetemplates = $errortypetemplates; } /** @@ -67,7 +70,8 @@ public function export_for_template(renderer_base $output) { $data = new stdClass(); $data->cmid = $this->cmid; $data->participants = $this->participants; - $data->errortypes = $this->errortypes; + $data->margicerrortypes = $this->margicerrortypes; + $data->errortypetemplates = $this->errortypetemplates; return $data; } diff --git a/db/install.xml b/db/install.xml index 66be3e2..ecc7fa4 100644 --- a/db/install.xml +++ b/db/install.xml @@ -106,13 +106,13 @@ - + - +
{{#str}}participant, mod_margic{{/str}} {{name}}
{{#defaulttype}}(S){{/defaulttype}} {{^defaulttype}}(M){{/defaulttype}} {{#canbeedited}} - - + + {{/canbeedited}}
diff --git a/db/upgrade.php b/db/upgrade.php index 79c3b53..433e087 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -91,7 +91,7 @@ function xmldb_margic_upgrade($oldversion) { // Add the order field to the table margic_errortypes. $table = new xmldb_table('margic_annotation_types'); - $field = new xmldb_field('order', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'userid'); + $field = new xmldb_field('order', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { @@ -106,7 +106,5 @@ function xmldb_margic_upgrade($oldversion) { upgrade_mod_savepoint(true, 2022072100, 'margic'); } - - return true; } diff --git a/errortypes.php b/errortypes.php index 8df5722..885e2c6 100644 --- a/errortypes.php +++ b/errortypes.php @@ -36,6 +36,9 @@ // Module instance ID as alternative. $m = optional_param('m', null, PARAM_INT); +// If template (1) or margic (2) error type. +$mode = optional_param('mode', 1, PARAM_INT); + // ID of type that should be edited. $edit = optional_param('edit', 0, PARAM_INT); @@ -71,9 +74,15 @@ $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); if ($edit !== 0) { - $editedtype = $DB->get_record('margic_errortypes', array('id' => $edit)); - if ($editedtype && (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) - || (isset($editedtype->defaulttype) && isset($editedtype->userid) && $editedtype->defaulttype == 0 && $editedtype->userid == $USER->id)) { + if ($mode == 1) { // If type is template error type. + $editedtype = $DB->get_record('margic_errortype_templates', array('id' => $edit)); + } else if ($mode == 2) { // If type is margic error type. + $editedtype = $DB->get_record('margic_errortypes', array('id' => $edit)); + } + + if ($editedtype && $mode == 2 || + ((isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) + || (isset($editedtype->defaulttype) && isset($editedtype->userid) && $editedtype->defaulttype == 0 && $editedtype->userid == $USER->id))) { $editedtypeid = $edit; $editedtypename = $editedtype->name; $editedcolor = $editedtype->color; @@ -82,12 +91,16 @@ } // Instantiate form. -$mform = new errortypes_form(null, array('editdefaulttype' => has_capability('mod/margic:editdefaulterrortypes', $context))); +$mform = new errortypes_form(null, array('editdefaulttype' => has_capability('mod/margic:editdefaulterrortypes', $context), 'mode' => $mode)); if (isset($editedtypeid)) { - $mform->set_data(array('id' => $id, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor, 'defaulttype' => $editeddefaulttype)); + if ($mode == 1) { // If type is template error type. + $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype)); + } else if ($mode == 2) { + $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor)); + } } else { - $mform->set_data(array('id' => $id)); + $mform->set_data(array('id' => $id, 'mode' => $mode)); } if ($mform->is_cancelled()) { @@ -102,7 +115,7 @@ $errortype->name = format_text($fromform->typename, 1, array('para' => false)); $errortype->color = $fromform->color; - if ($fromform->defaulttype === 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { + if (isset($fromform->standardtype) && $fromform->standardtype === 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { $errortype->userid = 0; $errortype->defaulttype = 1; } else { @@ -110,39 +123,57 @@ $errortype->defaulttype = 0; } - $errortype->order = 0; // Temp. - $errortype->unused = 0; - $errortype->replaces = null; + if ($mode == 2) { // If type is margic error type. + $errortype->priority = count($margic->get_margic_errortypes()) + 1; + $errortype->margic = $moduleinstance->id; + } - $DB->insert_record('margic_errortypes', $errortype); + if ($mode == 1) { // If type is template error type. + $DB->insert_record('margic_errortype_templates', $errortype); + + } else if ($mode == 2) { // If type is margic error type. + $DB->insert_record('margic_errortypes', $errortype); + } redirect($redirecturl, get_string('errortypeadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else if ($fromform->typeid !== 0 && isset($fromform->typename)) { // Update existing annotation type. - $errortype = $DB->get_record('margic_errortypes', array('id' => $fromform->typeid)); - if ($errortype && (isset($errortype->defaulttype) && $errortype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) - || (isset($errortype->defaulttype) && isset($errortype->userid) && $errortype->defaulttype == 0 && $errortype->userid == $USER->id)) { + if ($mode == 1) { // If type is template error type. + $errortype = $DB->get_record('margic_errortype_templates', array('id' => $fromform->typeid)); + } else if ($mode == 2) { // If type is margic error type. + $errortype = $DB->get_record('margic_errortypes', array('id' => $fromform->typeid)); + } + + if ($errortype && + ($mode == 2 || + (isset($errortype->defaulttype) && $errortype->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) + || (isset($errortype->defaulttype) && isset($errortype->userid) && $errortype->defaulttype == 0 && $errortype->userid == $USER->id))) { + $errortype->timemodified = time(); $errortype->name = format_text($fromform->typename, 1, array('para' => false)); $errortype->color = $fromform->color; - if (has_capability('mod/margic:editdefaulterrortypes', $context)) { + if ($mode == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { global $USER; - if ($fromform->defaulttype === 1 && $errortype->defaulttype !== $fromform->defaulttype) { + if ($fromform->standardtype === 1 && $errortype->defaulttype !== $fromform->standardtype) { $errortype->defaulttype = 1; $errortype->userid = 0; - } else if ($fromform->defaulttype === 0 && $errortype->defaulttype !== $fromform->defaulttype) { + } else if ($fromform->standardtype === 0 && $errortype->defaulttype !== $fromform->standardtype) { $errortype->defaulttype = 0; $errortype->userid = $USER->id; } + } else { + $errortype->defaulttype = 0; + $errortype->userid = $USER->id; } - $errortype->order = 0; // Temp. - $errortype->unused = 0; - $errortype->replaces = null; + if ($mode == 1) { // If type is template error type. + $DB->update_record('margic_errortype_templates', $errortype); + } else if ($mode == 2) { // If type is margic error type. + $DB->update_record('margic_errortypes', $errortype); + } - $DB->update_record('margic_errortypes', $errortype); redirect($redirecturl, get_string('errortypeedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { redirect($redirecturl, get_string('errortypecantbeedited', 'mod_margic'), null, notification::NOTIFY_ERROR); @@ -160,12 +191,24 @@ $PAGE->set_url('/mod/margic/errortypes.php', array('id' => $cm->id)); +$navtitle = ''; + + if (isset($editedtypeid)) { - $PAGE->navbar->add(get_string('editerrortype', 'mod_margic')); + $navtitle = get_string('editerrortype', 'mod_margic'); } else { - $PAGE->navbar->add(get_string('adderrortype', 'mod_margic')); + $navtitle = get_string('adderrortype', 'mod_margic'); } +if ($mode == 1) { // If type is template error type. + $navtitle .= ' (' . get_string('template', 'mod_margic') . ')'; +} else if ($mode == 2) { // If type is margic error type. + $navtitle .= ' (' . get_string('modulename', 'mod_margic') . ')'; +} + +$PAGE->navbar->add($navtitle); + + $PAGE->set_title(get_string('modulename', 'mod_margic').': ' . $margicname); $PAGE->set_heading(format_string($course->fullname)); $PAGE->set_context($context); @@ -178,8 +221,8 @@ echo $OUTPUT->box(format_module_intro('margic', $moduleinstance, $cm->id), 'generalbox mod_introbox', 'newmoduleintro'); } -if (isset($editedtypeid)) { - echo $OUTPUT->notification(get_string('changesforall', 'mod_margic'), notification::NOTIFY_WARNING); +if (isset($editedtypeid) && $mode == 1) { + echo $OUTPUT->notification(get_string('changetype', 'mod_margic'), notification::NOTIFY_WARNING); } $mform->display(); diff --git a/errortypes_form.php b/errortypes_form.php index a258166..81aa9a1 100644 --- a/errortypes_form.php +++ b/errortypes_form.php @@ -48,6 +48,9 @@ public function definition() { $mform->addElement('hidden', 'id', null); $mform->setType('id', PARAM_INT); + $mform->addElement('hidden', 'mode', 1); + $mform->setType('mode', PARAM_INT); + $mform->addElement('hidden', 'typeid', null); $mform->setType('typeid', PARAM_INT); @@ -64,13 +67,15 @@ public function definition() { $mform->addRule('color', null, 'required', null, 'client'); $mform->addHelpButton('color', 'explanationhexcolor', 'mod_margic'); - if ($this->_customdata['editdefaulttype']) { - $mform->addElement('advcheckbox', 'defaulttype', get_string('defaulttype', 'mod_margic'), get_string('explanationdefaulttype', 'mod_margic')); - } else { - $mform->addElement('hidden', 'defaulttype', 0); - } + if ($this->_customdata['mode'] == 1) { // If template error type. + if ($this->_customdata['editdefaulttype']) { + $mform->addElement('advcheckbox', 'standardtype', get_string('standardtype', 'mod_margic'), get_string('explanationstandardtype', 'mod_margic')); + } else { + $mform->addElement('hidden', 'standardtype', 0); + } - $mform->setType('defaulttype', PARAM_INT); + $mform->setType('standardtype', PARAM_INT); + } $this->add_action_buttons(); } diff --git a/lang/de/margic.php b/lang/de/margic.php index 5bbd405..fe475bc 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -194,13 +194,15 @@ $string['annotationssummary'] = 'Annotationsauswertung und Fehlertypen'; $string['participant'] = 'TeilnehmerIn'; $string['backtooverview'] = 'Zurück zur Übersicht'; -$string['adderrortype'] = 'Fehlertypen anlegen'; +$string['adderrortype'] = 'Fehlertyp anlegen'; $string['errortypeadded'] = 'Fehlertyp angelegt'; $string['editerrortype'] = 'Fehlertyp bearbeiten'; $string['errortypeedited'] = 'Fehlertyp bearbeitet'; +$string['editerrortypetemplate'] = 'Vorlage bearbeiten'; $string['errortypecantbeedited'] = 'Fehlertyp konnte nicht geändert werden'; $string['deleteerrortype'] = 'Fehlertyp entfernen'; $string['errortypedeleted'] = 'Fehlertyp entfernt'; +$string['deleteerrortypetemplate'] = 'Vorlage löschen'; $string['errortypeinvalid'] = 'Fehlertyp ungültig'; $string['nameoferrortype'] = 'Name des Fehlertyps'; $string['annotationcreated'] = 'Erstellt am {$a}'; @@ -208,15 +210,19 @@ $string['editannotation'] = 'Bearbeiten'; $string['deleteannotation'] = 'Löschen'; $string['annotationcolor'] = 'Farbe des Fehlertyps'; -$string['defaulttype'] = 'Standard Fehlertyp'; -$string['customtype'] = 'Eigener Fehlertyp'; +$string['standardtype'] = 'Standard Fehlertyp'; +$string['manualtype'] = 'Manueller Fehlertyp'; +$string['standard'] = 'Standard'; +$string['custom'] = 'Benutzerdefiniert'; +$string['type'] = 'Art'; +$string['color'] = 'Farbe'; $string['errnohexcolor'] = 'Kein hexadezimaler Farbwert.'; -$string['changesforall'] = 'Die Änderung des Namens oder der Farbe des Fehlertypen wirkt sich sofort nach dem Speichern auf alle bereits Angelegten sowie alle zukünftigen Annotationen aus.'; +$string['changetype'] = 'Die Änderung des Namens oder der Farbe des Fehlertypen wirkt sich nur auf die Vorlage aus und wird daher erst bei der Erstellung neuer Margics wirksam. Die Fehlertypen in bestehenden Margics sind von diesen Änderungen nicht betroffen.'; $string['explanationtypename'] = 'Name des Fehlertyps'; $string['explanationtypename_help'] = 'Der Name des Fehlertypen. Für folgende Standardfehlertypen sind bereits Übersetzungen in Moodle hinterlegt: "grammar_verb", "grammar_syntax", "grammar_congruence", "grammar_other", "expression", "orthography", "punctuation" und "other". Alle anderen Namen werden nicht übersetzt.'; $string['explanationhexcolor'] = 'Farbe des Fehlertyps'; $string['explanationhexcolor_help'] = 'Die Farbe des Fehlertypen als Hexadezimalwert. Dieser besteht aus genau 6 Zeichen (A-F sowie 0-9) und repräsentiert eine Farbe. Den Hexwert von beliebigen Farben kann man z. B. unter https://www.w3schools.com/colors/colors_picker.asp herausfinden.'; -$string['explanationdefaulttype'] = 'Hier kann ausgewählt werden, ob der Fehlertyp ein Standardtyp sein soll. In diesem Fall wird er allen Lehrenden in allen Margic-Instanzen angezeigt und kann von diesen verwendet werden. Andernfalls wird er ein normaler Fehlertyp und kann nur vom Ersteller verwendet werden.'; +$string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Fehlertyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre Margics ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren Margics verwendet werden.'; $string['annotatedtextnotfound'] = 'Annotierter Text nicht gefunden'; $string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist (z. B. durch eine nachträgliche Änderung des ursprünglichen Beitrags) ungültig geworden. Die Markierung für diese Annotierung muss deshalb neu gesetzt werden.'; $string['notallowedtodothis'] = 'Vorgang nicht möglich.'; @@ -241,6 +247,15 @@ $string['deleteallratings'] = 'Nur alle Bewertungen löschen'; $string['ratingsdeleted'] = 'Alle Bewertungen gelöscht'; +$string['margicerrortypes'] = 'Margic Fehlertypen'; +$string['errortypetemplates'] = 'Fehlertyp-Vorlagen'; +$string['errortypes'] = 'Fehlertypen'; +$string['template'] = 'Vorlage'; +$string['addtomargic'] = 'Zum Margic hinzufügen'; +$string['switchtotemplatetypes'] = 'Zu den Fehlertyp-Vorlagen wechseln'; +$string['switchtomargictypes'] = 'Zu den Fehlertypen des Margics wechseln'; +$string['notemplatetypes'] = 'Keine Fehlertyp-Vorlagen verfügbar'; + // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; $string['privacy:metadata:margic_annotations'] = 'Enthält die in allen Margics gemacht Annotationen.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index f618305..aa91468 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -207,29 +207,35 @@ $string['annotationssummary'] = 'Annotations summary and error types'; $string['participant'] = 'Participant'; $string['backtooverview'] = 'Back to overview'; -$string['adderrortype'] = 'Add annotation type'; -$string['errortypeadded'] = 'Annotation type added'; -$string['editerrortype'] = 'Edit annotation type'; -$string['errortypeedited'] = 'Annotation type edited'; -$string['errortypecantbeedited'] = 'Annotation type could not be changed'; -$string['deleteerrortype'] = 'Delete annotation type'; -$string['errortypedeleted'] = 'Annotation type deleted'; -$string['errortypeinvalid'] = 'Annotation type invalid'; -$string['nameoferrortype'] = 'Name of annotation type'; +$string['adderrortype'] = 'Add error type'; +$string['errortypeadded'] = 'Error type added'; +$string['editerrortype'] = 'Edit error type'; +$string['errortypeedited'] = 'Error type edited'; +$string['editerrortypetemplate'] = 'Edit template'; +$string['errortypecantbeedited'] = 'Error type could not be changed'; +$string['deleteerrortype'] = 'Delete error type'; +$string['errortypedeleted'] = 'Error type deleted'; +$string['deleteerrortypetemplate'] = 'Delete template'; +$string['errortypeinvalid'] = 'Error type invalid'; +$string['nameoferrortype'] = 'Name of error type'; $string['annotationcreated'] = 'Created at {$a}'; $string['annotationmodified'] = 'Modified at {$a}'; $string['editannotation'] = 'Edit'; $string['deleteannotation'] = 'Delete'; -$string['annotationcolor'] = 'Color of the annotation type'; -$string['defaulttype'] = 'Default error type'; -$string['customtype'] = 'Custom error type'; +$string['annotationcolor'] = 'Color of the error type'; +$string['standardtype'] = 'Standard error type'; +$string['manualtype'] = 'Manual error type'; +$string['standard'] = 'Standard'; +$string['custom'] = 'Custom'; +$string['type'] = 'Type'; +$string['color'] = 'Color'; $string['errnohexcolor'] = 'No hex value for color.'; -$string['changesforall'] = 'Changing the name or color of the annotation type will affect all already created annotations as well as all future annotations immediately after saving.'; +$string['changetype'] = 'Changing the name or color of the error type only affects the template and therefore only takes effect when new margics are created. The error types in existing margics are not affected by these changes.'; $string['explanationtypename'] = 'Name of annotation type'; $string['explanationtypename_help'] = 'The name of the annotation type. For the following standard annotation types, translations are already stored in Moodle: "grammar_verb", "grammar_syntax", "grammar_congruence", "grammar_other", "expression", "orthography", "punctuation" and "other". All other names are not translated.'; $string['explanationhexcolor'] = 'Color of the annotation type'; $string['explanationhexcolor_help'] = 'The color of the annotation type as hexadecimal value. This consists of exactly 6 characters (A-F as well as 0-9) and represents a color. You can find out the hexadecimal value of any color, for example, at https://www.w3schools.com/colors/colors_picker.asp.'; -$string['explanationdefaulttype'] = 'Here you can select whether the annotation type should be a default type. In this case it will be displayed to all teachers in all Margic instances and can be used by them. Otherwise, it becomes a normal error type and can only be used by its creator.'; +$string['explanationstandardtype'] = 'Here you can select whether the error type should be a default type. In this case teachers can select it as error type that can be used in their Margics. Otherwise, only you can add this error type to your Margics.'; $string['annotatedtextnotfound'] = 'Annotated text not found'; $string['annotatedtextinvalid'] = 'The originally annotated text has become invalid (e.g. due to a subsequent change to the original entry). The marking for this annotation must therefore be redone.'; $string['notallowedtodothis'] = 'No permissions to do this.'; @@ -254,6 +260,15 @@ $string['deleteallratings'] = 'Delete only all ratings'; $string['ratingsdeleted'] = 'All ratings deleted'; +$string['margicerrortypes'] = 'Margic error types'; +$string['errortypetemplates'] = 'Error type templates'; +$string['errortypes'] = 'Error types'; +$string['template'] = 'Template'; +$string['addtomargic'] = 'Add to Margic'; +$string['switchtotemplatetypes'] = 'Switch to the errortype templates'; +$string['switchtomargictypes'] = 'Switch to the error types for the Margic'; +$string['notemplatetypes'] = 'No errortype templates available'; + // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; $string['privacy:metadata:margic_annotations'] = 'Contains the annotations made in all margics.'; diff --git a/lib.php b/lib.php index 524565d..4d5093d 100644 --- a/lib.php +++ b/lib.php @@ -30,8 +30,7 @@ * will create a new instance and return the id number * of the new instance. * - * @param object $margic - * Object containing required margic properties. + * @param object $margic Object containing required margic properties. * @return int margic ID. */ function margic_add_instance($margic) { @@ -40,7 +39,7 @@ function margic_add_instance($margic) { if (empty($margic->assessed)) { $margic->assessed = 0; } - // 20190917 First one always true as ratingtime does not exist. + if (empty($margic->ratingtime) || empty($margic->assessed)) { $margic->assesstimestart = 0; $margic->assesstimefinish = 0; @@ -48,16 +47,32 @@ function margic_add_instance($margic) { $margic->timemodified = time(); $margic->id = $DB->insert_record('margic', $margic); - // 20200903 Added calendar dates. + // Add calendar dates. results::margic_update_calendar($margic, $margic->coursemodule); - // 20200901 Added expected completion date. + // Add expected completion date. if (! empty($margic->completionexpected)) { \core_completion\api::update_completion_date_event($margic->coursemodule, 'margic', $margic->id, $margic->completionexpected); } margic_grade_item_update($margic); + if (isset($margic->errortypes) && !empty($margic->errortypes)) { + // Add errortypes for margic. + $priority = 1; + foreach ($margic->errortypes as $id => $checked) { + if ($checked) { + $type = $DB->get_record('margic_errortype_templates', array('id' => $id)); + $type->margic = $margic->id; + $type->priority = $priority; + + $priority += 1; + + $DB->insert_record('margic_errortypes', $type); + } + } + } + return $margic->id; } @@ -66,8 +81,7 @@ function margic_add_instance($margic) { * Given an object containing all the necessary margic data, * will update an existing instance with new margic data. * - * @param object $margic - * Object containing required margic properties. + * @param object $margic Object containing required margic properties. * @return boolean True if successful. */ function margic_update_instance($margic) { @@ -165,6 +179,9 @@ function margic_delete_instance($id) { // Delete annotations. $DB->delete_records("margic_annotations", array("margic" => $margic->id)); + // Delete error types for margic. + $DB->delete_records("margic_errortypes", array("margic" => $margic->id)); + // Delete margic, else return false. if (!$DB->delete_records("margic", array("id" => $margic->id))) { return false; diff --git a/locallib.php b/locallib.php index cb2b189..e27366c 100644 --- a/locallib.php +++ b/locallib.php @@ -111,8 +111,7 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $this->annotations = $DB->get_records('margic_annotations', array('margic' => $this->get_course_module()->instance)); - $select = "defaulttype = 1"; - $select .= " OR userid = " . $USER->id; + $select = "margic = " . $this->instance->id; $this->errortypes = (array) $DB->get_records_select('margic_errortypes', $select); foreach ($this->annotations as $key => $annotation) { @@ -474,7 +473,7 @@ public function get_annotations() { * * @return array action */ - public function get_all_errortypes() { + public function get_margic_errortypes() { return $this->errortypes; } @@ -492,16 +491,27 @@ public function get_errortypes_for_form() { } else { $types[$key] = $type->name; } - - // if (in_array($this->id, json_decode($types[$key]->unused))) { - // unset($types[$key]); - // } - } return $types; } + /** + * Returns all errortype templates. + * + * @return array action + */ + public function get_all_errortype_templates() { + global $USER, $DB; + + $select = "defaulttype = 1"; + $select .= " OR userid = " . $USER->id; + + $errortypetemplates = (array) $DB->get_records_select('margic_errortype_templates', $select); + + return $errortypetemplates; + } + /** * Returns the entries for the margic instance grouped after pagecount. * diff --git a/mod_form.php b/mod_form.php index 671b851..0e46872 100644 --- a/mod_form.php +++ b/mod_form.php @@ -40,7 +40,7 @@ class mod_margic_mod_form extends moodleform_mod { * @return void */ public function definition() { - global $COURSE; + global $COURSE, $DB, $USER; $mform = &$this->_form; @@ -77,6 +77,33 @@ public function definition() { $mform->addElement('selectyesno', 'editdates', get_string('editdates', 'margic')); $mform->addHelpButton('editdates', 'editdates', 'margic'); + // Add the header for the error types. + $mform->addElement('header', 'errortypeshdr', get_string('errortypes', 'margic')); + + $select = "defaulttype = 1"; + $select .= " OR userid = " . $USER->id; + $errortypetemplates = (array) $DB->get_records_select('margic_errortype_templates', $select); + + $strmanager = get_string_manager(); + + $this->add_checkbox_controller(1); + + foreach ($errortypetemplates as $id => $type) { + if ($type->defaulttype == 1) { + $name = '(S)'; + } else { + $name = '(M)'; + } + + if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_margic')) { + $name .= '' . get_string($type->name, 'mod_margic') . ''; + } else { + $name .= '' . $type->name . ''; + } + + $mform->addElement('advcheckbox', 'errortypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); + } + // Add the header for appearance. $mform->addElement('header', 'appearancehdr', get_string('appearance')); diff --git a/templates/margic_annotations_summary.mustache b/templates/margic_annotations_summary.mustache index b2d1e13..f735565 100644 --- a/templates/margic_annotations_summary.mustache +++ b/templates/margic_annotations_summary.mustache @@ -24,34 +24,94 @@ {{#js}} {{/js}} -
- {{#str}}adderrortype, mod_margic{{/str}} +
+
+ + {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}modulename, mod_margic{{/str}}) +
+ +

{{#str}}margicerrortypes, mod_margic{{/str}}

+ + + + + {{#margicerrortypes}} + + {{/margicerrortypes}} + + + {{#participants}} + + {{#errors}}{{/errors}} + {{/participants}} + +
{{#str}}participant, mod_margic{{/str}} + {{name}} +
+ {{#defaulttype}}(S){{/defaulttype}} + {{^defaulttype}}(M){{/defaulttype}} + {{#canbeedited}} + + + {{/canbeedited}} +
+
{{firstname}} {{lastname}}{{.}}
- - - - {{#errortypes}} - - {{/errortypes}} - - - {{#participants}} - - {{#errors}}{{/errors}} - {{/participants}} - -
{{#str}}participant, mod_margic{{/str}} - {{name}} -
- {{#defaulttype}}(S){{/defaulttype}} - {{^defaulttype}}(M){{/defaulttype}} - {{#canbeedited}} - - - {{/canbeedited}} -
-
{{firstname}} {{lastname}}{{.}}
+
+
+ + {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}template, mod_margic{{/str}}) +
+ +

{{#str}}errortypetemplates, mod_margic{{/str}}

+ + {{#errortypetemplates.0}} + + + + + + + + + + + {{#errortypetemplates}} + + + + + + + + + {{/errortypetemplates}} + +
+ {{#str}}name{{/str}} + + {{#str}}type, mod_margic{{/str}} + + {{#str}}color, mod_margic{{/str}} + + {{#str}}edit{{/str}} + + {{#str}}delete{{/str}} + + {{#str}}addtomargic, mod_margic{{/str}} +
{{name}} + {{type}} + + {{#canbeedited}}{{/canbeedited}} + + {{#canbeedited}}{{/canbeedited}} + + +
+ {{/errortypetemplates.0}} + {{^errortypetemplates.0}} {{#str}}notemplatetypes, mod_margic{{/str}} {{/errortypetemplates.0}} +
{{#str}}backtooverview, mod_margic{{/str}} From e9e5809fe1bffcd23be082d5b7497299c7cafe62 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 25 Jul 2022 13:52:37 +0200 Subject: [PATCH 12/60] feat (errortypes): working on feature --- annotations_summary.php | 63 +++++++++++++++++-- errortypes.php | 5 +- lang/de/margic.php | 4 ++ lang/en/margic.php | 4 ++ lib.php | 20 +++--- locallib.php | 2 +- mod_form.php | 62 +++++++++--------- styles.css | 2 +- templates/margic_annotations_summary.mustache | 2 + 9 files changed, 116 insertions(+), 48 deletions(-) diff --git a/annotations_summary.php b/annotations_summary.php index 86c3b74..9df390d 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -33,16 +33,20 @@ $id = required_param('id', PARAM_INT); // Module instance ID as alternative. -$m = optional_param('m', null, PARAM_INT); +$m = optional_param('m', null, PARAM_INT); // ID of type that should be deleted. -$delete = optional_param('delete', 0, PARAM_INT); +$delete = optional_param('delete', 0, PARAM_INT); // ID of type that should be deleted. -$addtomargic = optional_param('addtomargic', 0, PARAM_INT); +$addtomargic = optional_param('addtomargic', 0, PARAM_INT); + +// ID of type where priority should be changed. +$priority = optional_param('priority', 0, PARAM_INT); +$action = optional_param('action', 0, PARAM_INT); // If template (1) or margic (2) error type. -$mode = optional_param('mode', null, PARAM_INT); +$mode = optional_param('mode', null, PARAM_INT); $margic = margic::get_margic_instance($id, $m, false, 'currententry', 0, 1); @@ -73,6 +77,7 @@ require_capability('mod/margic:makeannotations', $context); +// Add type to margic. if ($addtomargic) { $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); @@ -97,6 +102,56 @@ } } +// Change priority. +if ($mode == 2 && $priority && $action && $DB->record_exists('margic_errortypes', array('id' => $priority))) { + $redirecturl = new moodle_url('/mod/margic/annotations_summary.php', array('id' => $id)); + + $type = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'id' => $priority)); + + $prioritychanged = false; + $oldpriority = 0; + + if ($type && $action == 1 && $type->priority != 1) { // Increase priority (show more in front) + $oldpriority = $type->priority; + $type->priority -= 1; + $prioritychanged = true; + + $typeswitched = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'priority' => $type->priority)); + + if (!$typeswitched) { // If no type with priority+1 search for types with hihgher priority values + $typeswitched = $DB->get_records_select('margic_errortypes', "margic = $moduleinstance->id AND priority < $type->priority", null, 'priority ASC'); + $typeswitched = $typeswitched[array_key_last($typeswitched)]; + } + + } else if ($type && $action == 2 && $type->priority != $DB->count_records('margic_errortypes', array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back) + $oldpriority = $type->priority; + $type->priority += 1; + $prioritychanged = true; + + $typeswitched = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'priority' => $type->priority)); + + if (!$typeswitched) { // If no type with priority+1 search for types with hihgher priority values + $typeswitched = $DB->get_records_select('margic_errortypes', "margic = $moduleinstance->id AND priority > $type->priority", null, 'priority ASC'); + $typeswitched = $typeswitched[array_key_first($typeswitched)]; + } + } else { + redirect($redirecturl, get_string('prioritynotchanged', 'mod_margic'), null, notification::NOTIFY_ERROR); + } + + if ($typeswitched) { + // Update priority for type. + $DB->update_record('margic_errortypes', $type); + + // Update priority for type that type is switched with. + $typeswitched->priority = $oldpriority; + $DB->update_record('margic_errortypes', $typeswitched); + + redirect($redirecturl, get_string('prioritychanged', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + } else { + redirect($redirecturl, get_string('prioritynotchanged', 'mod_margic'), null, notification::NOTIFY_ERROR); + } +} + // Delete annotation. if ($delete !== 0 && $mode) { diff --git a/errortypes.php b/errortypes.php index 885e2c6..27f1acd 100644 --- a/errortypes.php +++ b/errortypes.php @@ -124,7 +124,7 @@ } if ($mode == 2) { // If type is margic error type. - $errortype->priority = count($margic->get_margic_errortypes()) + 1; + $errortype->priority = $margic->get_margic_errortypes()[array_key_last($margic->get_margic_errortypes())]->priority + 1; $errortype->margic = $moduleinstance->id; } @@ -162,9 +162,6 @@ $errortype->defaulttype = 0; $errortype->userid = $USER->id; } - } else { - $errortype->defaulttype = 0; - $errortype->userid = $USER->id; } if ($mode == 1) { // If type is template error type. diff --git a/lang/de/margic.php b/lang/de/margic.php index fe475bc..bbd4ae6 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -255,6 +255,10 @@ $string['switchtotemplatetypes'] = 'Zu den Fehlertyp-Vorlagen wechseln'; $string['switchtomargictypes'] = 'Zu den Fehlertypen des Margics wechseln'; $string['notemplatetypes'] = 'Keine Fehlertyp-Vorlagen verfügbar'; +$string['movefor'] = 'Weiter vorne anzeigen'; +$string['moveback'] = 'Weiter hinten anzeigen'; +$string['prioritychanged'] = 'Reihenfolge geändert'; +$string['prioritynotchanged'] = 'Reihenfolge konnte nicht geändert werden'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index aa91468..bb06360 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -268,6 +268,10 @@ $string['switchtotemplatetypes'] = 'Switch to the errortype templates'; $string['switchtomargictypes'] = 'Switch to the error types for the Margic'; $string['notemplatetypes'] = 'No errortype templates available'; +$string['movefor'] = 'Display more in front'; +$string['moveback'] = 'Display further back'; +$string['prioritychanged'] = 'Order changed'; +$string['prioritynotchanged'] = 'Order could not be changed'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/lib.php b/lib.php index 4d5093d..685f432 100644 --- a/lib.php +++ b/lib.php @@ -461,7 +461,7 @@ function margic_user_complete($course, $user, $mod, $margic) { * @param int $timestart * @return bool */ -function margic_print_recent_activity($course, $viewfullnames, $timestart) { +/* function margic_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT; if (! get_config('margic', 'showrecentactivity')) { @@ -557,7 +557,7 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { print_recent_activity_note($submission->timemodified, $submission, $cm->name, $link, false, $viewfullnames); } return true; -} +} */ /** * Implementation of the function for printing the form elements that control @@ -915,7 +915,7 @@ function margic_scale_used_anywhere($scaleid) { * @param int $groupid * @return int count($margics) Count of margic entries. */ -function margic_count_entries($margic, $groupid = 0) { +/* function margic_count_entries($margic, $groupid = 0) { global $DB; $cm = margic_get_coursemodule($margic->id); @@ -957,7 +957,7 @@ function margic_count_entries($margic, $groupid = 0) { } return count($margics); -} +} */ /** * Return entries that have not been emailed. @@ -965,7 +965,7 @@ function margic_count_entries($margic, $groupid = 0) { * @param int $cutofftime * @return object */ -function margic_get_unmailed_graded($cutofftime) { +/* function margic_get_unmailed_graded($cutofftime) { global $DB; $sql = "SELECT de.*, d.course, d.name FROM {margic_entries} de @@ -974,7 +974,7 @@ function margic_get_unmailed_graded($cutofftime) { return $DB->get_records_sql($sql, array( $cutofftime )); -} +} */ /** * Return margic log info. @@ -982,7 +982,7 @@ function margic_get_unmailed_graded($cutofftime) { * @param string $log * @return object */ -function margic_log_info($log) { +/* function margic_log_info($log) { global $DB; $sql = "SELECT d.*, u.firstname, u.lastname @@ -993,7 +993,7 @@ function margic_log_info($log) { return $DB->get_record_sql($sql, array( $log->info )); -} +} */ /** * Returns the margic instance course_module id. @@ -1001,7 +1001,7 @@ function margic_log_info($log) { * @param integer $margicid * @return object */ -function margic_get_coursemodule($margicid) { +/* function margic_get_coursemodule($margicid) { global $DB; return $DB->get_record_sql("SELECT cm.id FROM {course_modules} cm @@ -1009,7 +1009,7 @@ function margic_get_coursemodule($margicid) { WHERE cm.instance = ? AND m.name = 'margic'", array( $margicid )); -} +} */ /** * Serves the margic files. diff --git a/locallib.php b/locallib.php index e27366c..2683eab 100644 --- a/locallib.php +++ b/locallib.php @@ -112,7 +112,7 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $this->annotations = $DB->get_records('margic_annotations', array('margic' => $this->get_course_module()->instance)); $select = "margic = " . $this->instance->id; - $this->errortypes = (array) $DB->get_records_select('margic_errortypes', $select); + $this->errortypes = (array) $DB->get_records_select('margic_errortypes', $select, null, 'priority ASC'); foreach ($this->annotations as $key => $annotation) { diff --git a/mod_form.php b/mod_form.php index 0e46872..09a1f58 100644 --- a/mod_form.php +++ b/mod_form.php @@ -40,7 +40,7 @@ class mod_margic_mod_form extends moodleform_mod { * @return void */ public function definition() { - global $COURSE, $DB, $USER; + global $DB, $USER; $mform = &$this->_form; @@ -54,6 +54,39 @@ public function definition() { $this->standard_intro_elements(get_string('margicdescription', 'margic')); + $id = optional_param('update', null, PARAM_INT); + + if (!isset($id) || $id == 0) { + // Add the header for the error types. + $mform->addElement('header', 'errortypeshdr', get_string('errortypes', 'margic')); + $mform->setExpanded('errortypeshdr'); + + $select = "defaulttype = 1"; + $select .= " OR userid = " . $USER->id; + $errortypetemplates = (array) $DB->get_records_select('margic_errortype_templates', $select); + + $strmanager = get_string_manager(); + + $this->add_checkbox_controller(1); + + foreach ($errortypetemplates as $id => $type) { + if ($type->defaulttype == 1) { + $name = '(S)'; + } else { + $name = '(M)'; + } + + if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_margic')) { + $name .= '' . get_string($type->name, 'mod_margic') . ''; + } else { + $name .= '' . $type->name . ''; + } + + $mform->addElement('advcheckbox', 'errortypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); + } + + } + // Add the header for availability. $mform->addElement('header', 'availibilityhdr', get_string('availability')); @@ -77,33 +110,6 @@ public function definition() { $mform->addElement('selectyesno', 'editdates', get_string('editdates', 'margic')); $mform->addHelpButton('editdates', 'editdates', 'margic'); - // Add the header for the error types. - $mform->addElement('header', 'errortypeshdr', get_string('errortypes', 'margic')); - - $select = "defaulttype = 1"; - $select .= " OR userid = " . $USER->id; - $errortypetemplates = (array) $DB->get_records_select('margic_errortype_templates', $select); - - $strmanager = get_string_manager(); - - $this->add_checkbox_controller(1); - - foreach ($errortypetemplates as $id => $type) { - if ($type->defaulttype == 1) { - $name = '(S)'; - } else { - $name = '(M)'; - } - - if ($type->defaulttype == 1 && $strmanager->string_exists($type->name, 'mod_margic')) { - $name .= '' . get_string($type->name, 'mod_margic') . ''; - } else { - $name .= '' . $type->name . ''; - } - - $mform->addElement('advcheckbox', 'errortypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); - } - // Add the header for appearance. $mform->addElement('header', 'appearancehdr', get_string('appearance')); diff --git a/styles.css b/styles.css index 8781221..e35215b 100644 --- a/styles.css +++ b/styles.css @@ -173,7 +173,7 @@ } #page-mod-margic-annotations_summary th { - min-width: 100px; + min-width: 135px; } #page-mod-margic-view .annotatedtextpreviewdiv { diff --git a/templates/margic_annotations_summary.mustache b/templates/margic_annotations_summary.mustache index f735565..71d839a 100644 --- a/templates/margic_annotations_summary.mustache +++ b/templates/margic_annotations_summary.mustache @@ -45,6 +45,8 @@ {{/canbeedited}} + +
{{/margicerrortypes}} From 720de54cbf32697e585f50c7d8831f59344c8a00 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 25 Jul 2022 19:27:43 +0200 Subject: [PATCH 13/60] feat (errortypes): last changes in privacy and backup api reflecting changes in errortypes feature --- .../backup_margic_activity_task.class.php | 12 +++++----- backup/moodle2/backup_margic_stepslib.php | 19 +++++++++++---- .../restore_margic_activity_task.class.php | 4 ++-- backup/moodle2/restore_margic_stepslib.php | 23 +++++++++++++++++-- classes/privacy/provider.php | 19 ++++++++------- db/install.xml | 2 +- db/upgrade.php | 4 ++-- lang/de/margic.php | 14 ++++++----- lang/en/margic.php | 16 +++++++------ lib.php | 15 +++++++++++- version.php | 2 +- 11 files changed, 87 insertions(+), 43 deletions(-) diff --git a/backup/moodle2/backup_margic_activity_task.class.php b/backup/moodle2/backup_margic_activity_task.class.php index 8332bb0..df9fb4f 100644 --- a/backup/moodle2/backup_margic_activity_task.class.php +++ b/backup/moodle2/backup_margic_activity_task.class.php @@ -65,17 +65,17 @@ public static function encode_content_links($content) { $search = "/(".$base."\/mod\/margic\/view.php\?id\=)([0-9]+)(&|&)userid=([0-9]+)/"; $content = preg_replace($search, '$@MARGICVIEWBYID*$2*$4@$', $content); - // Link to the edit page with optional entryid of entry that should be edited. - $search = "/(".$base."\/mod\/margic\/edit.php\?id\=)([0-9]+)(&|&)entryid=([0-9]+)/"; - $content = preg_replace($search, '$@MARGICEDITVIEW*$2*$4@$', $content); + // Link to the edit page. + $search = "/(".$base."\/mod\/margic\/edit.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@MARGICEDITVIEW*$2@$', $content); // Link to the annotation summary by moduleid. $search = "/(".$base."\/mod\/margic\/annotations_summary.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@MARGICANNOTATIONSUMMARY*$2@$', $content); - // Link to the page for editing annotation types with optional id of tyoe that should be edited. - $search = "/(".$base."\/mod\/margic\/errortypes.php\?id\=)([0-9]+)(&|&)edit=([0-9]+)/"; - $content = preg_replace($search, '$@MARGICERRORTYPES*$2*$4@$', $content); + // Link to the page for editing errortypes. + $search = "/(".$base."\/mod\/margic\/errortypes.php\?id\=)([0-9]+)/"; + $content = preg_replace($search, '$@MARGICERRORTYPES*$2@$', $content); return $content; } diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php index a10911a..0a56c4b 100644 --- a/backup/moodle2/backup_margic_stepslib.php +++ b/backup/moodle2/backup_margic_stepslib.php @@ -42,15 +42,17 @@ protected function define_structure() { 'scale', 'assessed', 'assesstimestart', 'assesstimefinish', 'timeopen', 'timeclose', 'editall', 'editdates', 'annotationareawidth')); - $entries = new backup_nested_element('entries'); + $errortypes = new backup_nested_element('errortypes'); + $errortype = new backup_nested_element('errortype', array('id'), array( + 'timecreated', 'timemodified', 'name', 'color', 'defaulttype', 'userid', 'priority')); + $entries = new backup_nested_element('entries'); $entry = new backup_nested_element('entry', array('id'), array( 'userid', 'timecreated', 'timemodified', 'text', 'format', 'rating', 'entrycomment', 'formatcomment', 'teacher', 'timemarked', 'mailed')); $annotations = new backup_nested_element('annotations'); - $annotation = new backup_nested_element('annotation', array('id'), array( 'userid', 'timecreated', 'timemodified', 'type', 'startcontainer', 'endcontainer', 'startposition', 'endposition', 'text')); @@ -64,22 +66,28 @@ protected function define_structure() { 'timecreated', 'timemodified')); // Build the tree with these elements with $margic as the root of the backup tree. + $margic->add_child($errortypes); + $errortypes->add_child($errortype); + $margic->add_child($entries); $entries->add_child($entry); + $margic->add_child($tags); + $tags->add_child($tag); + $entry->add_child($annotations); $annotations->add_child($annotation); $entry->add_child($ratings); $ratings->add_child($rating); - $margic->add_child($tags); - $tags->add_child($tag); - // Define the source tables for the elements. $margic->set_source_table('margic', array('id' => backup::VAR_ACTIVITYID)); + // Errortypes. + $errortype->set_source_table('margic_errortypes', array('margic' => backup::VAR_PARENTID)); + if ($userinfo) { // Entries. @@ -116,6 +124,7 @@ protected function define_structure() { $margic->annotate_ids('scale', 'scale'); $rating->annotate_ids('scale', 'scaleid'); $rating->annotate_ids('user', 'userid'); + $errortype->annotate_ids('user', 'userid'); if ($userinfo) { $entry->annotate_ids('user', 'userid'); diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php index bec8fd8..736f6e8 100644 --- a/backup/moodle2/restore_margic_activity_task.class.php +++ b/backup/moodle2/restore_margic_activity_task.class.php @@ -74,9 +74,9 @@ public static function define_decode_rules() { $rules[] = new restore_decode_rule('MARGICINDEX', '/mod/margic/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('MARGICVIEWBYID', '/mod/margic/view.php?id=$1&userid=$2', array('course_module', 'userid')); - $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1&entryid=$2', array('course_module', 'entryid')); + $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1', array('course_module')); $rules[] = new restore_decode_rule('MARGICANNOTATIONSUMMARY', '/mod/margic/annotations_summary.php?id=$1', 'course_module'); - $rules[] = new restore_decode_rule('MARGICERRORTYPES', '/mod/margic/errortypes.php?id=$1&edit=$2', array('course_module', 'edit')); + $rules[] = new restore_decode_rule('MARGICERRORTYPES', '/mod/margic/errortypes.php?id=$1', array('course_module')); return $rules; } diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php index 29fd9af..6fc8fcb 100644 --- a/backup/moodle2/restore_margic_stepslib.php +++ b/backup/moodle2/restore_margic_stepslib.php @@ -41,6 +41,7 @@ protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('margic', '/activity/margic'); + $paths[] = new restore_path_element('margic_errortype', '/activity/margic/errortypes/errortype'); if ($userinfo) { $paths[] = new restore_path_element('margic_entry', '/activity/margic/entries/entry'); @@ -66,8 +67,6 @@ protected function process_margic($data) { $oldid = $data->id; $data->course = $this->get_courseid(); - error_log('process_margic'); - // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. if (!isset($data->assesstimestart)) { @@ -119,6 +118,26 @@ protected function process_margic_entry($data) { $this->set_mapping('margic_entry', $oldid, $newitemid); } + /** + * Restore margic errortype. + * + * @param object $data data. + */ + protected function process_margic_errortype($data) { + global $DB; + + error_log('process_margic_errortype'); + + $data = (object) $data; + $oldid = $data->id; + + $data->margic = $this->get_new_parentid('margic'); + $data->userid = $this->get_mappingid('user', $data->userid); + + $newitemid = $DB->insert_record('margic_errortypes', $data); + $this->set_mapping('margic_errortype', $oldid, $newitemid); + } + /** * Add annotations to restored margic entries. * diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 5924669..6a30207 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -80,14 +80,14 @@ public static function get_metadata(collection $items): collection { 'text' => 'privacy:metadata:margic_annotations:text', ], 'privacy:metadata:margic_annotations'); - // The table 'margic_errortypes' stores the annotation types of all margics. - $items->add_database_table('margic_errortypes', [ - 'userid' => 'privacy:metadata:margic_errortypes:userid', - 'timecreated' => 'privacy:metadata:margic_errortypes:timecreated', - 'timemodified' => 'privacy:metadata:margic_errortypes:timemodified', - 'name' => 'privacy:metadata:margic_errortypes:name', - 'color' => 'privacy:metadata:margic_errortypes:color', - ], 'privacy:metadata:margic_errortypes'); + // The table 'margic_errortype_templates' stores the errirtype templatess of all margics. + $items->add_database_table('margic_errortype_templates', [ + 'timecreated' => 'privacy:metadata:margic_errortype_templates:timecreated', + 'timemodified' => 'privacy:metadata:margic_errortype_templates:timemodified', + 'name' => 'privacy:metadata:margic_errortype_templates:name', + 'color' => 'privacy:metadata:margic_errortype_templates:color', + 'userid' => 'privacy:metadata:margic_errortype_templates:userid', + ], 'privacy:metadata:margic_errortype_templates'); // The margic uses multiple subsystems that save personal data. $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); @@ -140,8 +140,6 @@ public static function get_contexts_for_userid(int $userid): contextlist { $contextlist->add_from_sql($sql, $params); - // TODO: Get errortypes for margic. - return $contextlist; } @@ -237,6 +235,7 @@ public static function export_user_data(approved_contextlist $contextlist) { self::export_entries_data($userid, $margic->id, $margic->contextid); self::export_annotations_data($userid, $margic->id, $margic->contextid); + } } diff --git a/db/install.xml b/db/install.xml index ecc7fa4..9d98219 100644 --- a/db/install.xml +++ b/db/install.xml @@ -106,7 +106,7 @@ - + diff --git a/db/upgrade.php b/db/upgrade.php index 433e087..2209e1d 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -89,9 +89,9 @@ function xmldb_margic_upgrade($oldversion) { $dbman->create_table($table); } - // Add the order field to the table margic_errortypes. + // Add the priority field to the table margic_errortypes. $table = new xmldb_table('margic_annotation_types'); - $field = new xmldb_field('order', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid'); + $field = new xmldb_field('priority', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { diff --git a/lang/de/margic.php b/lang/de/margic.php index bbd4ae6..77167ef 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -242,6 +242,8 @@ $string['entryadded'] = 'Eintrag angelegt oder bearbeitet'; $string['deletealluserdata'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags löschen'; $string['alluserdatadeleted'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags wurden entfernt'; +$string['deleteerrortypes'] = 'Fehlertypen löschen'; +$string['errortypesdeleted'] = 'Fehlertypen gelöscht'; $string['deletealltags'] = 'Nur alle Tags löschen'; $string['tagsdeleted'] = 'Alle Tags gelöscht'; $string['deleteallratings'] = 'Nur alle Bewertungen löschen'; @@ -263,7 +265,7 @@ // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; $string['privacy:metadata:margic_annotations'] = 'Enthält die in allen Margics gemacht Annotationen.'; -$string['privacy:metadata:margic_errortypes'] = 'Enthält die Fehlertypen aus allen Margics.'; +$string['privacy:metadata:margic_errortype_templates'] = 'Enthält die von Lehrenden angelegten Fehlertyp-Vorlagen.'; $string['privacy:metadata:margic_entries:margic'] = 'ID des Margic, zu dem der Eintrag gehört.'; $string['privacy:metadata:margic_entries:userid'] = 'ID des Benutzers, zu dem der Eintrag gehört.'; $string['privacy:metadata:margic_entries:timecreated'] = 'Datum, an dem der Eintrag erstellt wurde.'; @@ -280,11 +282,11 @@ $string['privacy:metadata:margic_annotations:timemodified'] = 'Zeitpunkt der letzten Änderung der Annotation.'; $string['privacy:metadata:margic_annotations:type'] = 'ID des Typs der Annotation.'; $string['privacy:metadata:margic_annotations:text'] = 'Inhalt der Annotation.'; -$string['privacy:metadata:margic_errortypes:userid'] = 'ID des Benutzers, der den Fehlertyp erstellt hat.'; -$string['privacy:metadata:margic_errortypes:timecreated'] = 'Datum, an dem der Fehlertyp erstellt wurde.'; -$string['privacy:metadata:margic_errortypes:timemodified'] = 'Zeitpunkt der letzten Änderung des Fehlertyps.'; -$string['privacy:metadata:margic_errortypes:name'] = 'Name des Fehlertyps.'; -$string['privacy:metadata:margic_errortypes:color'] = 'Farbe des Fehlertyps als Hex-Wert.'; +$string['privacy:metadata:margic_errortype_templates:timecreated'] = 'Datum, an dem die Fehlertyp-Vorlage erstellt wurde.'; +$string['privacy:metadata:margic_errortype_templates:timemodified'] = 'Zeitpunkt der letzten Änderung der Fehlertyp-Vorlage.'; +$string['privacy:metadata:margic_errortype_templates:name'] = 'Name der Fehlertyp-Vorlage.'; +$string['privacy:metadata:margic_errortype_templates:color'] = 'Farbe der Fehlertyp-Vorlage als Hex-Wert.'; +$string['privacy:metadata:margic_errortype_templates:userid'] = 'ID des Benutzers, der die Fehlertyp-Vorlage erstellt hat.'; $string['privacy:metadata:core_rating'] = 'Die zu den Margic-Einträgen hinzugefügten Bewertungen werden unter Verwendung des core_rating-Systems gespeichert.'; $string['privacy:metadata:core_files'] = 'Dateien, die mit Margic-Einträgen verknüpft sind, werden mithilfe des Systems core_files gespeichert.'; $string['privacy:metadata:preference:sortoption'] = 'Die Präferenz für die Sortierung jedes Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index bb06360..1018cff 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -255,6 +255,8 @@ $string['entryadded'] = 'Entry added or modified.'; $string['deletealluserdata'] = 'Delete all entries, annotations, files, ratings and tags'; $string['alluserdatadeleted'] = 'All entries, annotations, files, ratings and tags are deleted'; +$string['deleteerrortypes'] = 'Delete errortypes'; +$string['errortypesdeleted'] = 'Errortypes deleted'; $string['deletealltags'] = 'Delete only all tags'; $string['tagsdeleted'] = 'All tags deleted'; $string['deleteallratings'] = 'Delete only all ratings'; @@ -276,7 +278,7 @@ // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; $string['privacy:metadata:margic_annotations'] = 'Contains the annotations made in all margics.'; -$string['privacy:metadata:margic_errortypes'] = 'Contains the annotation types of all margics.'; +$string['privacy:metadata:margic_errortype_templates'] = 'Contains the errortype templates created by teachers.'; $string['privacy:metadata:margic_entries:margic'] = 'ID of the Margic the entry belongs to.'; $string['privacy:metadata:margic_entries:userid'] = 'ID of the user the entry belongs to.'; $string['privacy:metadata:margic_entries:timecreated'] = 'Date on which the entry was created.'; @@ -291,13 +293,13 @@ $string['privacy:metadata:margic_annotations:userid'] = 'ID of the user that made the annotation.'; $string['privacy:metadata:margic_annotations:timecreated'] = 'Date on which the annotation was created.'; $string['privacy:metadata:margic_annotations:timemodified'] = 'Time the annotation was last modified.'; -$string['privacy:metadata:margic_annotations:type'] = 'Id of the type of the annotation.'; +$string['privacy:metadata:margic_annotations:type'] = 'ID of the type of the annotation.'; $string['privacy:metadata:margic_annotations:text'] = 'Content of the annotation.'; -$string['privacy:metadata:margic_errortypes:userid'] = 'ID of the user that made the annotation type.'; -$string['privacy:metadata:margic_errortypes:timecreated'] = 'Date on which the annotation type was created.'; -$string['privacy:metadata:margic_errortypes:timemodified'] = 'Time the annotation type was last modified.'; -$string['privacy:metadata:margic_errortypes:name'] = 'Name of the annotation type.'; -$string['privacy:metadata:margic_errortypes:color'] = 'Color of the annotation type as hex value.'; +$string['privacy:metadata:margic_errortype_templates:timecreated'] = 'Date on which the errortype template was created.'; +$string['privacy:metadata:margic_errortype_templates:timemodified'] = 'Time the errortype template was last modified.'; +$string['privacy:metadata:margic_errortype_templates:name'] = 'Name of the errortype template.'; +$string['privacy:metadata:margic_errortype_templates:color'] = 'Color of the errortype template as hex value.'; +$string['privacy:metadata:margic_errortype_templates:userid'] = 'ID of the user that made the errortype template.'; $string['privacy:metadata:core_rating'] = 'Ratings added to margic entries are stored using the core_rating system.'; $string['privacy:metadata:core_files'] = 'Files linked to margic entries are stored using the core_files system.'; $string['privacy:metadata:preference:sortoption'] = 'The preference for the sorting of each margic.'; diff --git a/lib.php b/lib.php index 685f432..dff5669 100644 --- a/lib.php +++ b/lib.php @@ -569,6 +569,8 @@ function margic_reset_course_form_definition(&$mform) { $mform->addElement('header', 'margicheader', get_string('modulenameplural', 'margic')); $mform->addElement('checkbox', 'reset_margic_all', get_string('deletealluserdata', 'margic')); + $mform->addElement('checkbox', 'reset_margic_errortypes', get_string('deleteerrortypes', 'margic')); + $mform->addElement('checkbox', 'reset_margic_ratings', get_string('deleteallratings', 'margic')); $mform->disabledIf('reset_margic_ratings', 'reset_margic_all', 'checked'); $mform->setAdvanced('reset_margic_ratings'); @@ -585,7 +587,7 @@ function margic_reset_course_form_definition(&$mform) { * @return array */ function margic_reset_course_form_defaults($course) { - return array('reset_margic_all' => 1, 'reset_margic_ratings' => 0, 'reset_margic_tags' => 0); + return array('reset_margic_all' => 1, 'reset_margic_errortypes' => 1, 'reset_margic_ratings' => 0, 'reset_margic_tags' => 0); } /** @@ -664,6 +666,14 @@ function margic_reset_userdata($data) { ); } + // Delete errortypes. + if (!empty($data->reset_margic_errortypes) ) { + $DB->delete_records_select('margic_errortypes', "margic IN ($sql)", $params); + + $status[] = array('component' => $modulename, 'item' => get_string('errortypesdeleted', 'margic'), 'error' => false); + + } + // Delete ratings only. if (!empty($data->reset_margic_ratings) ) { @@ -683,6 +693,9 @@ function margic_reset_userdata($data) { if (empty($data->reset_gradebook_grades)) { margic_reset_gradebook($data->courseid); } + + $status[] = array('component' => $modulename, 'item' => get_string('ratingsdeleted', 'margic'), 'error' => false); + } // Delete tags only. diff --git a/version.php b/version.php index 1458b8d..b60a2a1 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'mod_margic'; $plugin->release = '1.1.3'; // User-friendly version number. -$plugin->version = 2022072100; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022072400; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; From e698a242834f2127c9f868173d69a57f467d508d Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 26 Jul 2022 13:12:47 +0200 Subject: [PATCH 14/60] feat (printview): optimizations for print view --- styles.css | 14 +++++++++++++- templates/margic_view.mustache | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/styles.css b/styles.css index e35215b..a6bcf50 100644 --- a/styles.css +++ b/styles.css @@ -202,4 +202,16 @@ padding: 0; margin-right: 5px; margin-bottom: 5px; -} \ No newline at end of file +} + +@media print { + .actionbuttons, + .activity-navigation, + #page-footer { + display: none; + } + + #page { + margin-top: 0px !important; + } +} diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 24c1a52..9d7f595 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -44,7 +44,7 @@ {{/entries.0}}
-
+
{{^edittimehasended}}{{^edittimenotstarted}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimenotstarted}}{{/edittimehasended}} {{#entries.0}} {{#canmanageentries}}{{/canmanageentries}} From ec5f215a3b2d40cc032e19461f849ea5dc3219b1 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 26 Jul 2022 14:39:35 +0200 Subject: [PATCH 15/60] feat (annotations): some changes for focusing annotations --- amd/src/annotations.js | 2 ++ templates/margic_view.mustache | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/amd/src/annotations.js b/amd/src/annotations.js index d8c6d51..20af884 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -82,6 +82,8 @@ $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid); $('.annotationarea-' + entry + ' .annotation-form').show(); $('.annotationarea-' + entry + ' #id_text').focus(); + } else { + $('.annotation-box-' + annotationid).focus(); } } diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 9d7f595..b60a56c 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -156,7 +156,7 @@

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}} -
+
{{type}} From a9c8aa24c8a2193e777e944e4a350195fc87aadb Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 26 Jul 2022 14:43:20 +0200 Subject: [PATCH 16/60] feat (annotations): some changes for focusing annotations --- amd/build/annotations.min.js | 13 +++++++++++-- amd/build/annotations.min.js.map | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 5d157da..f97e6c0 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,2 +1,11 @@ -function _createForOfIteratorHelper(a){if("undefined"==typeof Symbol||null==a[Symbol.iterator]){if(Array.isArray(a)||(a=_unsupportedIterableToArray(a))){var b=0,c=function(){};return{s:c,n:function n(){if(b>=a.length)return{done:!0};return{done:!1,value:a[b++]}},e:function e(a){throw a},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var d,e=!0,f=!1,g;return{s:function s(){d=a[Symbol.iterator]()},n:function n(){var a=d.next();e=a.done;return a},e:function e(a){f=!0;g=a},f:function f(){try{if(!e&&null!=d.return)d.return()}finally{if(f)throw g}}}}function _unsupportedIterableToArray(a,b){if(!a)return;if("string"==typeof a)return _arrayLikeToArray(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return _arrayLikeToArray(a,b)}function _arrayLikeToArray(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c=a.comparePoint(b,0)&&0<=a.comparePoint(b,e)}catch(a){return!1}}function j(a){var b=a.nodeName.toLowerCase(),c=b;if("#text"===b){c="text()"}return c}function k(a){var b=0,c=a;while(c){if(c.nodeName===a.nodeName){b+=1}c=c.previousSibling}return b}function l(a){var b=j(a),c=k(a);return"".concat(b,"[").concat(c,"]")}function m(a,b){var c="",d=a;while(d!==b){if(!d){throw new Error("Node is not a descendant of root")}c=l(d)+"/"+c;d=d.parentNode}c="/"+c;c=c.replace(/\/$/,"");return c}function n(a,b,c){b=b.toUpperCase();for(var d=-1,e=0,f;ej){return null}}else{i=h;j=0}var m=n(e,i,j);if(!m){return null}e=m}}catch(a){f.e(a)}finally{f.f()}return e}function p(a){var b=1=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from($("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"file":"annotations.min.js"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"names":["define","$","init","annotations","canmakeannotations","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file From d35d3408f56b15dc5c5c848d475a8ca4a1abe548 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 26 Jul 2022 15:14:56 +0200 Subject: [PATCH 17/60] chore (js): chore in js file for moodle guidelines --- amd/build/annotations.min.js | 1 - amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 141 +++++++++++---------- annotations_summary.php | 4 +- backup/moodle2/restore_margic_stepslib.php | 2 - errortypes.php | 3 +- locallib.php | 4 - templates/margic_view.mustache | 2 +- 8 files changed, 84 insertions(+), 75 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index f97e6c0..352b4f5 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -3,7 +3,6 @@ function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof * Module for the annotation functions of the margic. * * @module mod_margic/annotations - * @package mod_margic * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */define("mod_margic/annotations",["jquery"],(function($){return{init:function(annotations,canmakeannotations){function editAnnotation(annotationid){if(canmakeannotations){removeAllTempHighlights(),resetForms();var entry=annotations[annotationid].entry;$(".annotation-box-"+annotationid).hide(),$(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),$(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),$(".annotation-form-"+entry+' input[name="startposition"]').val(annotations[annotationid].startposition),$(".annotation-form-"+entry+' input[name="endposition"]').val(annotations[annotationid].endposition),$(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),$(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),$(".annotation-form-"+entry+" select").val(annotations[annotationid].type),$("#annotationpreview-temp-"+entry).html($("#annotationpreview-"+annotationid).html()),$("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),$(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),$(".annotationarea-"+entry+" .annotation-form").show(),$(".annotationarea-"+entry+" #id_text").focus()}else $(".annotation-box-"+annotationid).focus()}function resetForms(){$(".annotation-form").hide(),$('.annotation-form input[name^="annotationid"]').val(null),$('.annotation-form input[name^="startcontainer"]').val(-1),$('.annotation-form input[name^="endcontainer"]').val(-1),$('.annotation-form input[name^="startposition"]').val(-1),$('.annotation-form input[name^="endposition"]').val(-1),$('.annotation-form textarea[name^="text"]').val(""),$(".annotation-box").not(".annotation-form").show()}function wholeTextNodesInRange(range){if(range.collapsed)return[];var root=range.commonAncestorContainer;if(root.nodeType!==Node.ELEMENT_NODE&&(root=root.parentElement),!root)return[];for(var node,textNodes=[],nodeIter=root.ownerDocument.createNodeIterator(root,NodeFilter.SHOW_TEXT);node=nodeIter.nextNode();)if(isNodeInRange(range,node)){var text=node;text===range.startContainer&&range.startOffset>0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from($("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @package mod_margic\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms\n $('.annotation-form').hide();\n\n // remove col-mds from moodle form\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n function recreateAnnotations(){\n for (let annotation of Object.values(annotations)) {\n\n //recreate range from db\n var newrange = document.createRange();\n\n try {\n newrange.setStart(nodeFromXPath(annotation.startcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( \"#entry-\" + annotation.entry)[0]), annotation.endposition);\n }\n catch (e) {\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n function resetForms(){\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {string} cssClass - A CSS class to use for the highlight\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * CSS selector that will match the placeholder within a page/tile container.\n */\n //const placeholderSelector = '.annotator-placeholder';\n\n /**\n * Return true if `node` is inside a placeholder element created with `createPlaceholder`.\n *\n * This is typically used to test if a highlight element associated with an\n * anchor is inside a placeholder.\n *\n * @param {Node} node\n */\n // function isInPlaceholder(node) {\n // if (!node.parentElement) {\n // return false;\n // }\n // return node.parentElement.closest(placeholderSelector) !== null;\n // }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // nb. The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* namespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n *\n * @param {HTMLElement} root\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0){\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).addClass('hovered');\n $('.annotated-'+id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave (function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-'+id).removeClass('hovered');\n $('.annotated-'+id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave (function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-'+id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function(){\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function(){\n $('.annotated_temp').removeClass('hovered');\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.annotated', function(){\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // onclick listener for editing annotation\n $(document).on('click', '.edit-annotation', function(){\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e){\n e.preventDefault();\n\n removeAllTempHighlights(); // remove other temporary highlights\n\n resetForms(); // remove old form contents\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"names":["define","$","init","annotations","canmakeannotations","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"names":["define","$","init","annotations","canmakeannotations","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 20af884..d5b6313 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -17,7 +17,6 @@ * Module for the annotation functions of the margic. * * @module mod_margic/annotations - * @package mod_margic * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -26,26 +25,32 @@ return { init: function(annotations, canmakeannotations) { - // Hide all Moodle forms + // Hide all Moodle forms. $('.annotation-form').hide(); - // remove col-mds from moodle form + // Remove col-mds from moodle form. $('.annotation-form div.col-md-3').removeClass('col-md-3'); $('.annotation-form div.col-md-9').removeClass('col-md-9'); $('.annotation-form div.form-group').removeClass('form-group'); $('.annotation-form div.row').removeClass('row'); - function recreateAnnotations(){ + /** + * Recreate annotations. + * + */ + function recreateAnnotations() { for (let annotation of Object.values(annotations)) { - //recreate range from db + // Recreate range from db. var newrange = document.createRange(); try { - newrange.setStart(nodeFromXPath(annotation.startcontainer, $( "#entry-" + annotation.entry)[0]), annotation.startposition); - newrange.setEnd(nodeFromXPath(annotation.endcontainer, $( "#entry-" + annotation.entry)[0]), annotation.endposition); - } - catch (e) { + newrange.setStart( + nodeFromXPath(annotation.startcontainer, $("#entry-" + annotation.entry)[0]), annotation.startposition); + newrange.setEnd( + nodeFromXPath(annotation.endcontainer, $("#entry-" + annotation.entry)[0]), annotation.endposition); + } catch (e) { + // eslint-disable-line } var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color); @@ -56,6 +61,11 @@ } } + /** + * Edit annotation. + * + * @param {int} annotationid + */ function editAnnotation(annotationid) { if (canmakeannotations) { removeAllTempHighlights(); @@ -63,7 +73,7 @@ var entry = annotations[annotationid].entry; - $('.annotation-box-' + annotationid).hide(); // hide edited annotation-box + $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box. $('.annotation-form-' + entry + ' input[name="startcontainer"]').val(annotations[annotationid].startcontainer); $('.annotation-form-' + entry + ' input[name="endcontainer"]').val(annotations[annotationid].endcontainer); @@ -77,7 +87,7 @@ $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type); $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html()); - $('#annotationpreview-temp-' + entry).css( 'border-color', '#' + annotations[annotationid].color); + $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color); $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid); $('.annotationarea-' + entry + ' .annotation-form').show(); @@ -87,7 +97,10 @@ } } - function resetForms(){ + /** + * Reset all annotation forms + */ + function resetForms() { $('.annotation-form').hide(); $('.annotation-form input[name^="annotationid"]').val(null); @@ -99,7 +112,7 @@ $('.annotation-form textarea[name^="text"]').val(''); - $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation + $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation. } /** @@ -172,7 +185,9 @@ * element of the specified class and returns the highlight Elements. * * @param {Range} range - Range to be highlighted + * @param {int} annotationid - ID of annotation * @param {string} cssClass - A CSS class to use for the highlight + * @param {string} color - Color of the highlighting * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect */ function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') { @@ -213,6 +228,7 @@ if (annotationid) { highlightEl.className += ' ' + cssClass + '-' + annotationid; + // highlightEl.tabIndex = 1; highlightEl.id = cssClass + '-' + annotationid; highlightEl.style.backgroundColor = '#' + color; } @@ -232,6 +248,7 @@ * * @param {Range} range * @param {Node} node + * @return {bool} - If node is in range */ function isNodeInRange(range, node) { try { @@ -245,34 +262,15 @@ } catch (e) { // `comparePoint` may fail if the `range` and `node` do not share a common // ancestor or `node` is a doctype. - return false; + return false; } } - /** - * CSS selector that will match the placeholder within a page/tile container. - */ - //const placeholderSelector = '.annotator-placeholder'; - - /** - * Return true if `node` is inside a placeholder element created with `createPlaceholder`. - * - * This is typically used to test if a highlight element associated with an - * anchor is inside a placeholder. - * - * @param {Node} node - */ - // function isInPlaceholder(node) { - // if (!node.parentElement) { - // return false; - // } - // return node.parentElement.closest(placeholderSelector) !== null; - // } - /** * Get the node name for use in generating an xpath expression. * * @param {Node} node + * @return {string} - Name of the node */ function getNodeName(node) { const nodeName = node.nodeName.toLowerCase(); @@ -287,6 +285,7 @@ * Get the index of the node as it appears in its parent's child list * * @param {Node} node + * @return {int} - Position of the node */ function getNodePosition(node) { let pos = 0; @@ -301,6 +300,12 @@ return pos; } + /** + * Get the path segments to the node + * + * @param {Node} node + * @return {array} - Path segments + */ function getPathSegment(node) { const name = getNodeName(node); const pos = getNodePosition(node); @@ -313,6 +318,7 @@ * * @param {Node} node - The node to generate a path to * @param {Node} root - Root node to which the returned path is relative + * @return {string} - The xpath of a node */ function xpathFromNode(node, root) { let xpath = ''; @@ -339,6 +345,7 @@ * @param {Element} element * @param {string} nodeName * @param {number} index + * @return {Element|null} - The child element or null */ function nthChildOfType(element, nodeName, index) { nodeName = nodeName.toUpperCase(); @@ -438,11 +445,11 @@ '.' + xpath, root, - // nb. The `namespaceResolver` and `result` arguments are optional in the spec + // The `namespaceResolver` and `result` arguments are optional in the spec // but required in Edge Legacy. - null /* namespaceResolver */, + null /* NamespaceResolver */, XPathResult.FIRST_ORDERED_NODE_TYPE, - null /* result */ + null /* Result */ ).singleNodeValue; } } @@ -464,12 +471,10 @@ /** * Remove all temporary highlights under a given root element. - * - * @param {HTMLElement} root */ function removeAllTempHighlights() { const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); - if (highlights !== undefined && highlights.length != 0){ + if (highlights !== undefined && highlights.length != 0) { removeHighlights(highlights); } } @@ -496,14 +501,16 @@ if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) { - removeAllTempHighlights(); // remove other temporary highlights + removeAllTempHighlights(); // Remove other temporary highlights. - resetForms(); // remove old form contents + resetForms(); // Remove old form contents. var entry = this.id.replace(/entry-/, ''); - $('.annotation-form-' + entry + ' input[name="startcontainer"]').val(xpathFromNode(selectedrange.startContainer, this)); - $('.annotation-form-' + entry + ' input[name="endcontainer"]').val(xpathFromNode(selectedrange.endContainer, this)); + $('.annotation-form-' + entry + ' input[name="startcontainer"]').val( + xpathFromNode(selectedrange.startContainer, this)); + $('.annotation-form-' + entry + ' input[name="endcontainer"]').val( + xpathFromNode(selectedrange.endContainer, this)); $('.annotation-form-' + entry + ' input[name="startposition"]').val(selectedrange.startOffset); $('.annotation-form-' + entry + ' input[name="endposition"]').val(selectedrange.endOffset); @@ -523,64 +530,70 @@ recreateAnnotations(); // Highlight annotation and all annotated text if annotated text is hovered - $('.annotated').mouseenter (function() { + $('.annotated').mouseenter(function() { var id = this.id.replace('annotated-', ''); - $('.annotationpreview-'+id).addClass('hovered'); - $('.annotated-'+id).addClass('hovered'); + $('.annotationpreview-' + id).addClass('hovered'); + $('.annotated-' + id).addClass('hovered'); $('.annotation-box-' + id + ' .errortype').addClass('hovered'); }); - $('.annotated').mouseleave (function() { + $('.annotated').mouseleave(function() { var id = this.id.replace('annotated-', ''); - $('.annotationpreview-'+id).removeClass('hovered'); - $('.annotated-'+id).removeClass('hovered'); + $('.annotationpreview-' + id).removeClass('hovered'); + $('.annotated-' + id).removeClass('hovered'); $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); }); // Highlight annotated text if annotationpreview is hovered - $('.annotatedtextpreview').mouseenter (function() { + $('.annotatedtextpreview').mouseenter(function() { var id = this.id.replace('annotationpreview-', ''); - $('.annotated-'+id).addClass('hovered'); + $('.annotated-' + id).addClass('hovered'); }); - $('.annotatedtextpreview').mouseleave (function() { + $('.annotatedtextpreview').mouseleave(function() { var id = this.id.replace('annotationpreview-', ''); - $('.annotated-'+id).removeClass('hovered'); + $('.annotated-' + id).removeClass('hovered'); }); // Highlight whole temp annotation if part of temp annotation is hovered - $(document).on('mouseover', '.annotated_temp', function(){ + $(document).on('mouseover', '.annotated_temp', function() { $('.annotated_temp').addClass('hovered'); }); - $(document).on('mouseleave', '.annotated_temp', function(){ + $(document).on('mouseleave', '.annotated_temp', function() { $('.annotated_temp').removeClass('hovered'); }); - // onclick listener for editing annotation - $(document).on('click', '.annotated', function(){ + // Onclick listener for editing annotation. + $(document).on('click', '.annotated', function() { var id = this.id.replace('annotated-', ''); editAnnotation(id); }); - // onclick listener for editing annotation - $(document).on('click', '.edit-annotation', function(){ + // Onclick listener for editing annotation. + $(document).on('click', '.edit-annotation', function() { var id = this.id.replace('edit-annotation-', ''); editAnnotation(id); }); + // Onclick listener for click on annotation-box. + // $(document).on('click', '.annotation-box', function() { + // var id = this.id.replace('annotation-box-', ''); + // $('#annotated-' + id).focus(); + // }); + // onclick listener if form is canceled - $(document).on('click', '#id_cancel', function(e){ + $(document).on('click', '#id_cancel', function(e) { e.preventDefault(); - removeAllTempHighlights(); // remove other temporary highlights + removeAllTempHighlights(); // Remove other temporary highlights. - resetForms(); // remove old form contents + resetForms(); // Remove old form contents. }); // Listen for return key pressed to submit annotation form. - $('textarea').keypress(function (e) { + $('textarea').keypress(function(e) { if (e.which == 13) { $(this).parents(':eq(2)').submit(); e.preventDefault(); diff --git a/annotations_summary.php b/annotations_summary.php index 9df390d..0b6684d 100644 --- a/annotations_summary.php +++ b/annotations_summary.php @@ -123,7 +123,9 @@ $typeswitched = $typeswitched[array_key_last($typeswitched)]; } - } else if ($type && $action == 2 && $type->priority != $DB->count_records('margic_errortypes', array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back) + } else if ($type && $action == 2 && + $type->priority != $DB->count_records('margic_errortypes', array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back) + $oldpriority = $type->priority; $type->priority += 1; $prioritychanged = true; diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php index 6fc8fcb..e3ee1c9 100644 --- a/backup/moodle2/restore_margic_stepslib.php +++ b/backup/moodle2/restore_margic_stepslib.php @@ -170,7 +170,6 @@ protected function process_margic_entry_tag($data) { error_log('process_margic_entry_tag'); - if (! core_tag_tag::is_enabled('mod_margic', 'margic_entries')) { // Tags disabled in server, nothing to process. return; } @@ -237,6 +236,5 @@ protected function after_execute() { error_log('margic restore after_execute AFTERFEEDBACK'); - } } diff --git a/errortypes.php b/errortypes.php index 27f1acd..1e3fa25 100644 --- a/errortypes.php +++ b/errortypes.php @@ -95,7 +95,8 @@ if (isset($editedtypeid)) { if ($mode == 1) { // If type is template error type. - $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype)); + $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, + 'typename' => $editedtypename, 'color' => $editedcolor, 'standardtype' => $editeddefaulttype)); } else if ($mode == 2) { $mform->set_data(array('id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, 'color' => $editedcolor)); } diff --git a/locallib.php b/locallib.php index 2683eab..69b43b6 100644 --- a/locallib.php +++ b/locallib.php @@ -264,7 +264,6 @@ function sortannotation($a, $b) { return false; } - if ($a->position === $b->position) { return $a->startposition > $b->startposition; } @@ -292,7 +291,6 @@ function sortannotation($a, $b) { $this->entries[$i]->entrycanbeedited = false; } - // Index entry for annotation sorting. $position = 0; @@ -339,7 +337,6 @@ function sortannotation($a, $b) { $this->entries[$i]->annotations[$key]->canbeedited = false; } - // Get position of startcontainer. $xpath = new DOMXpath($doc); $nodelist = $xpath->query('/' . $annotation->startcontainer); @@ -352,7 +349,6 @@ function sortannotation($a, $b) { // var_dump($annotation->startcontainer); // echo "
"; - // var_dump('$nodelist'); // var_dump($nodelist); diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index b60a56c..0eb20b6 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -156,7 +156,7 @@

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}} -
+
{{type}} From 8b242429522034defa0c7a1d62e41934acf82cd7 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 26 Jul 2022 20:12:41 +0200 Subject: [PATCH 18/60] feat (edit): reworked the editing of entries so that the original entry with its annotations is kept and the edited entry is stored as new but connected entry --- classes/local/results.php | 10 +- classes/output/margic_view.php | 26 ++- db/install.xml | 1 + db/upgrade.php | 16 ++ edit.php | 25 ++- lang/de/margic.php | 3 +- lang/en/margic.php | 3 +- locallib.php | 238 +++++++++++++++------------ styles.css | 5 + templates/margic_childentry.mustache | 84 ++++++++++ templates/margic_entry.mustache | 131 +++++++++++++++ templates/margic_view.mustache | 101 +----------- version.php | 2 +- 13 files changed, 422 insertions(+), 223 deletions(-) create mode 100644 templates/margic_childentry.mustache create mode 100644 templates/margic_entry.mustache diff --git a/classes/local/results.php b/classes/local/results.php index c93d656..5100674 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -224,7 +224,8 @@ public static function download_entries($context, $course, $margic) { get_string('teacher', 'margic'), get_string('timemarked', 'margic'), get_string('mailed', 'margic'), - get_string('text', 'margic') + get_string('text', 'margic'), + get_string('preventry', 'margic') ); // Add the headings to our data array. $csv->add_data($fields); @@ -242,7 +243,8 @@ public static function download_entries($context, $course, $margic) { d.entrycomment AS entrycomment, d.teacher AS teacher, to_char(to_timestamp(d.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked, - d.mailed AS mailed + d.mailed AS mailed, + d.preventry AS preventry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid WHERE d.userid > 0 "; @@ -260,7 +262,8 @@ public static function download_entries($context, $course, $margic) { d.entrycomment AS entrycomment, d.teacher AS teacher, FROM_UNIXTIME(d.timemarked) AS TIMEMARKED, - d.mailed AS mailed + d.mailed AS mailed, + d.preventry AS preventry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid WHERE d.userid > 0 "; @@ -291,6 +294,7 @@ public static function download_entries($context, $course, $margic) { $d->teacher, $d->timemarked, $d->mailed, + $d->preventry, format_text($d->text, $d->format, array('para' => false)) ); $csv->add_data($output); diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 1dc6db0..dae7e6a 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -178,7 +178,8 @@ public function export_for_template(renderer_base $output) { foreach ($this->entries as $key => $entry) { if ($this->canmanageentries) { // Set user picture for teachers. - $this->entries[$key]->user->userpicture = $OUTPUT->user_picture($entry->user, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true)); + $this->entries[$key]->user->userpicture = $OUTPUT->user_picture($entry->user, + array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true)); } // Add feedback area to entry. @@ -205,6 +206,29 @@ public function export_for_template(renderer_base $output) { } else { $this->entries[$key]->annotationform = false; } + + // Add annotation form to child entries of entry. + foreach ($this->entries[$key]->childentries as $ck => $childentry) { + if ($this->annotationmode) { + + $mform = new \annotation_form(new \moodle_url('/mod/margic/annotations.php', array('id' => $this->cmid)), array('types' => $this->errortypes)); + + // Set default data. + $mform->set_data(array('id' => $this->cmid, 'entry' => $childentry->id)); + + $this->entries[$key]->childentries[$ck]->annotationform = $mform->render(); + + foreach ($this->entries[$key]->childentries[$ck]->annotations as $anr => $annotation) { + $annotater = $DB->get_record('user', array('id' => $annotation->userid)); + $annotaterimage = $OUTPUT->user_picture($annotater, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + + $this->entries[$key]->childentries[$ck]->annotations[$anr]->userpicturestr = $annotaterimage; + } + + } else { + $this->entries[$key]->childentries[$ck]->annotationform = false; + } + } } } diff --git a/db/install.xml b/db/install.xml index 9d98219..4cd788e 100644 --- a/db/install.xml +++ b/db/install.xml @@ -45,6 +45,7 @@ + diff --git a/db/upgrade.php b/db/upgrade.php index 2209e1d..ed36871 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -106,5 +106,21 @@ function xmldb_margic_upgrade($oldversion) { upgrade_mod_savepoint(true, 2022072100, 'margic'); } + if ($oldversion < 2022072600) { + + // Add the preventry field to the margic_entries table. + $table = new xmldb_table('margic_entries'); + $field = new xmldb_field('preventry', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'mailed'); + + // Conditionally launch add field. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Margic savepoint reached. + upgrade_mod_savepoint(true, 2022072600, 'margic'); + + } + return true; } diff --git a/edit.php b/edit.php index ca3d517..017d98f 100644 --- a/edit.php +++ b/edit.php @@ -144,23 +144,30 @@ $newentry->userid = $USER->id; $newentry->timecreated = $fromform->timecreated; - $newentry->timemodified = 0; $newentry->text = ''; $newentry->format = 1; - if ($fromform->entryid != 0 && $entry != false) { - - $newentry->id = $fromform->entryid; + if ($fromform->entryid != 0 && $entry != false) { // If existing entry is edited. + if (!isset($entry->preventry)) { + $newentry->preventry = $fromform->entryid; + } else { + $newentry->preventry = $entry->preventry; + } $newentry->entrycomment = $entry->entrycomment; $newentry->teacher = $entry->teacher; - $newentry->timemodified = $timenow; + + $newentry->timecreated = $entry->timecreated; $newentry->timemarked = $entry->timemarked; - } else { - if (! $newentry->id = $DB->insert_record("margic_entries", $newentry)) { - throw new moodle_exception(get_string('generalerrorinsert', 'margic')); - } + + // Update timemodified for parent entry. + $entry->timemodified = $timenow;; + $DB->update_record('margic_entries', $entry); + } + + if (! $newentry->id = $DB->insert_record("margic_entries", $newentry)) { + throw new moodle_exception(get_string('generalerrorinsert', 'margic')); } $fromform = file_postupdate_standard_editor($fromform, 'text', $editoroptions, $editoroptions['context'], 'mod_margic', 'entry', $newentry->id); diff --git a/lang/de/margic.php b/lang/de/margic.php index 77167ef..9a93cd8 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -33,7 +33,7 @@ $string['calendarend'] = '{$a} schließt'; $string['calendarstart'] = '{$a} öffnet'; $string['configdateformat'] = 'Damit wird festgelegt, wie Daten in Margic-Berichten angezeigt werden. Der Standardwert "M d, Y G:i" ist Monat, Tag, Jahr und Uhrzeit im 24-Stunden-Format. Weitere Beispiele und vordefinierte Datumskonstanten finden Sie unter Datum im PHP-Handbuch.'; -$string['created'] = 'Erstellt vor {$a->days} Tagen und {$a->hours} Stunden.'; +$string['created'] = 'vor {$a->years} Jahren, {$a->month} Monaten, {$a->days} Tagen und {$a->hours} Stunden'; $string['csvexport'] = 'Exportieren nach .csv'; $string['dateformat'] = 'Standard-Datumsformat'; $string['deadline'] = 'Offene Tage'; @@ -261,6 +261,7 @@ $string['moveback'] = 'Weiter hinten anzeigen'; $string['prioritychanged'] = 'Reihenfolge geändert'; $string['prioritynotchanged'] = 'Reihenfolge konnte nicht geändert werden'; +$string['revision'] = 'Überarbeitung'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 1018cff..ae1821e 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -45,7 +45,7 @@ $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; $string['configdateformat'] = 'This defines how dates are shown in margic reports. The default value, "M d, Y G:i" is Month, day, year and 24 hour format time. Refer to Date in the PHP manual for more examples and predefined date constants.'; -$string['created'] = 'Created {$a->days} days and {$a->hours} hours ago.'; +$string['created'] = '{$a->years} years, {$a->month} months, {$a->days} days and {$a->hours} hours ago'; $string['csvexport'] = 'Export to .csv'; $string['deadline'] = 'Days Open'; $string['dateformat'] = 'Default date format'; @@ -274,6 +274,7 @@ $string['moveback'] = 'Display further back'; $string['prioritychanged'] = 'Order changed'; $string['prioritynotchanged'] = 'Order could not be changed'; +$string['revision'] = 'Revision'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/locallib.php b/locallib.php index 69b43b6..d7a06fb 100644 --- a/locallib.php +++ b/locallib.php @@ -87,6 +87,28 @@ class margic { */ public function __construct($id, $m, $userid, $action, $pagecount, $page) { + // Custom sort function for annotations. + function sortannotation($a, $b) { + // var_dump($a); + // var_dump($b); + + if (!isset($a->position)) { + // var_dump('Fehler: keine Position an Element A'); + // var_dump($a->id); + return true; + } else if (!isset($b->position)) { + // var_dump('Fehler: keine Position an Element B'); + // var_dump($b->id); + return false; + } + + if ($a->position === $b->position) { + return $a->startposition > $b->startposition; + } + + return $a->position > $b->position; + } + global $DB, $USER; if (isset($id) && $id != 0) { @@ -230,16 +252,17 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $allowedusers = true; } + // Get entries. if ($this->mode == 'allentries') { if ($userid && $userid != 0) { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $userid), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $userid, 'preventry' => null), $sortoptions); } else { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'preventry' => null), $sortoptions); } } else if ($this->mode == 'ownentries') { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id, 'preventry' => null), $sortoptions); } $gradingstr = get_string('needsgrading', 'margic'); @@ -249,34 +272,27 @@ public function __construct($id, $m, $userid, $action, $pagecount, $page) { $strmanager = get_string_manager(); - // Custom sort function for annotations. - function sortannotation($a, $b) { - // var_dump($a); - // var_dump($b); - - if (!isset($a->position)) { - // var_dump('Fehler: keine Position an Element A'); - // var_dump($a->id); - return true; - } else if (!isset($b->position)) { - // var_dump('Fehler: keine Position an Element B'); - // var_dump($b->id); - return false; - } - - if ($a->position === $b->position) { - return $a->startposition > $b->startposition; - } - - return $a->position > $b->position; - } - + // Prepare entries. foreach ($this->entries as $i => $entry) { $this->entries[$i]->user = $DB->get_record('user', array('id' => $entry->userid)); if (!$currentgroups || ($allowedusers && in_array($this->entries[$i]->user, $allowedusers))) { + // Get child entries for entry. + $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'preventry' => $entry->id), 'timecreated ASC'); + + $revisionnr = 1; + foreach ($this->entries[$i]->childentries as $ci => $childentry) { + $this->entries[$i]->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager); + $this->entries[$i]->childentries[$ci]->revision = $revisionnr; + $revisionnr += 1; + } + + $this->entries[$i]->childentries = array_values($this->entries[$i]->childentries); + + // Get entry stats. $this->entries[$i]->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); + // Check entry grading. if (!empty($entry->timecreated) && empty($entry->timemarked)) { $this->entries[$i]->needsgrading = $gradingstr; } else if (!empty($entry->timemodified) && !empty($entry->timemarked) && $entry->timemodified > $entry->timemarked) { @@ -285,97 +301,18 @@ function sortannotation($a, $b) { $this->entries[$i]->needsregrading = false; } + // Check if entry can be edited. if ($viewinguserid == $entry->userid) { $this->entries[$i]->entrycanbeedited = true; } else { $this->entries[$i]->entrycanbeedited = false; } - // Index entry for annotation sorting. - $position = 0; - - $doc = new DOMDocument(); - $doc->loadHTML($this->entries[$i]->text); - - $this->index_original($doc); - - // var_dump('NEW ENTRY'); - // var_dump($i); - - // var_dump('
'); - // var_dump('
'); - - // var_dump('NEW nodepositions'); - // var_dump($this->nodepositions); - - // var_dump('
'); - // var_dump('
'); - - // Get annotations for entry. - $this->entries[$i]->annotations = array_values($DB->get_records('margic_annotations', array('margic' => $this->cm->instance, 'entry' => $entry->id))); - - foreach ($this->entries[$i]->annotations as $key => $annotation) { - - if (!$DB->record_exists('margic_errortypes', array('id' => $annotation->type))) { // If annotation type does not exist. - $this->entries[$i]->annotations[$key]->color = 'FFFF00'; - $this->entries[$i]->annotations[$key]->defaulttype = 0; - $this->entries[$i]->annotations[$key]->type = get_string('deletederrortype', 'mod_margic'); - } else { - $this->entries[$i]->annotations[$key]->color = $this->errortypes[$annotation->type]->color; - $this->entries[$i]->annotations[$key]->defaulttype = $this->errortypes[$annotation->type]->defaulttype; - - if ($this->entries[$i]->annotations[$key]->defaulttype == 1 && $strmanager->string_exists($this->errortypes[$annotation->type]->name, 'mod_margic')) { - $this->entries[$i]->annotations[$key]->type = get_string($this->errortypes[$annotation->type]->name, 'mod_margic'); - } else { - $this->entries[$i]->annotations[$key]->type = $this->errortypes[$annotation->type]->name; - } - } - - if (has_capability('mod/margic:makeannotations', $this->context) && $annotation->userid == $USER->id) { - $this->entries[$i]->annotations[$key]->canbeedited = true; - } else { - $this->entries[$i]->annotations[$key]->canbeedited = false; - } - - // Get position of startcontainer. - $xpath = new DOMXpath($doc); - $nodelist = $xpath->query('/' . $annotation->startcontainer); - - // echo('$annotation->id
'); - // var_dump($annotation->id); - // echo "
"; - - // echo('$annotation->startcontainer
'); - // var_dump($annotation->startcontainer); - // echo "
"; - - // var_dump('$nodelist'); - // var_dump($nodelist); - - // var_dump('$nodepositions'); - // var_dump($this->nodepositions); - - foreach ($this->nodepositions as $position => $node) { - if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array. - $this->entries[$i]->annotations[$key]->position = $position; // If so asssign its position to annotation. - // echo "POSITION OF ANNOTATION:
"; - // echo $this->entries[$i]->annotations[$key]->position; - // echo "
"; - break; - } - } - } - - // Sort annotations by position and offset of startcontainer. - usort($this->entries[$i]->annotations, "sortannotation"); - - // Reset nodepositions with empty array for next entry. - $this->nodepositions = array(); - + // Prepare entry annotations. + $this->entries[$i] = $this->prepare_entry_annotations($entry, $strmanager); } else { unset($this->entries[$i]); } - } } @@ -637,4 +574,91 @@ private function search_dom_node(DOMNode $domnode, &$position = 0) { } } } + + private function prepare_entry_annotations($entry, $strmanager) { + global $DB, $USER; + + // Index entry for annotation sorting. + $position = 0; + + $doc = new DOMDocument(); + $doc->loadHTML($entry->text); + + $this->index_original($doc); + + // var_dump('NEW ENTRY'); + // var_dump($i); + + // var_dump('
'); + // var_dump('
'); + + // var_dump('NEW nodepositions'); + // var_dump($this->nodepositions); + + // var_dump('
'); + // var_dump('
'); + + // Get annotations for entry. + $entry->annotations = array_values($DB->get_records('margic_annotations', array('margic' => $this->cm->instance, 'entry' => $entry->id))); + + foreach ($entry->annotations as $key => $annotation) { + + if (!$DB->record_exists('margic_errortypes', array('id' => $annotation->type))) { // If annotation type does not exist. + $entry->annotations[$key]->color = 'FFFF00'; + $entry->annotations[$key]->defaulttype = 0; + $entry->annotations[$key]->type = get_string('deletederrortype', 'mod_margic'); + } else { + $entry->annotations[$key]->color = $this->errortypes[$annotation->type]->color; + $entry->annotations[$key]->defaulttype = $this->errortypes[$annotation->type]->defaulttype; + + if ($entry->annotations[$key]->defaulttype == 1 && $strmanager->string_exists($this->errortypes[$annotation->type]->name, 'mod_margic')) { + $entry->annotations[$key]->type = get_string($this->errortypes[$annotation->type]->name, 'mod_margic'); + } else { + $entry->annotations[$key]->type = $this->errortypes[$annotation->type]->name; + } + } + + if (has_capability('mod/margic:makeannotations', $this->context) && $annotation->userid == $USER->id) { + $entry->annotations[$key]->canbeedited = true; + } else { + $entry->annotations[$key]->canbeedited = false; + } + + // Get position of startcontainer. + $xpath = new DOMXpath($doc); + $nodelist = $xpath->query('/' . $annotation->startcontainer); + + // echo('$annotation->id
'); + // var_dump($annotation->id); + // echo "
"; + + // echo('$annotation->startcontainer
'); + // var_dump($annotation->startcontainer); + // echo "
"; + + // var_dump('$nodelist'); + // var_dump($nodelist); + + // var_dump('$nodepositions'); + // var_dump($this->nodepositions); + + foreach ($this->nodepositions as $position => $node) { + if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array. + $entry->annotations[$key]->position = $position; // If so asssign its position to annotation. + // echo "POSITION OF ANNOTATION:
"; + // echo $entry->annotations[$key]->position; + // echo "
"; + break; + } + } + } + + // Sort annotations by position and offset of startcontainer. + usort($entry->annotations, "sortannotation"); + + // Reset nodepositions with empty array for next entry. + $this->nodepositions = array(); + + return $entry; + } } diff --git a/styles.css b/styles.css index a6bcf50..9d84068 100644 --- a/styles.css +++ b/styles.css @@ -204,6 +204,11 @@ margin-bottom: 5px; } +#page-mod-margic-view .childentrywrapper { + margin-top: -10px; + margin-left: 20px; +} + @media print { .actionbuttons, .activity-navigation, diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache new file mode 100644 index 0000000..d6c3a67 --- /dev/null +++ b/templates/margic_childentry.mustache @@ -0,0 +1,84 @@ +{{! + 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 . +}} +{{! + @copyright 2022 coactum GmbH + @template margic/margic_childentry + + Template for single child entry. +}} + +{{#js}} +{{/js}} + +
+
+
+
{{#str}}revision, mod_margic{{/str}} {{revision}} ({{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}) +
+ + {{#text}} +
+ {{{text}}} +
+ {{/text}} + {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#annotationmode}} +
+ {{#annotations}} +
+
+ + {{type}} + +
+
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}} +
+
+ {{/annotations}} + + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ {{{annotationform}}} +
+ {{/annotationform}} +
+ {{/annotationmode}} +
+
\ No newline at end of file diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache new file mode 100644 index 0000000..2e382e7 --- /dev/null +++ b/templates/margic_entry.mustache @@ -0,0 +1,131 @@ +{{! + 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 . +}} +{{! + @copyright 2022 coactum GmbH + @template margic/margic_entry + + Template for single entry. +}} + +{{#js}} +{{/js}} + +
+
+
+

{{#str}}entry, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} +

+ + {{#user}} + {{#canmanageentries}} + {{{userpicture}}} {{^singleuser}}{{/singleuser}} + {{/canmanageentries}} + {{/user}} + + {{#text}} +
+ {{{text}}} +
+ {{/text}} + {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#stats}} + {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} +
+ {{/stats}} + {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} + + {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}} +
+ + {{#edittimeends}} +
+ {{^edittimehasended}}{{#str}}editingends, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} + {{#edittimehasended}}{{#str}}editingended, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} +
+ {{/edittimeends}} + + {{#needsgrading}} +
{{needsgrading}}
+ {{/needsgrading}} + + {{#needsregrading}} +
{{needsregrading}}
+ {{/needsregrading}} + + {{#canmanageentries}} + {{#gradingform}} +
+ {{{gradingform}}} +
+ {{/gradingform}} + {{/canmanageentries}} + {{^canmanageentries}} + {{#gradingform}}{{{gradingform}}}{{/gradingform}} + {{/canmanageentries}} +
+ {{#annotationmode}} +
+

{{#str}} annotations, mod_margic {{/str}}

+ {{#annotations}} +
+
+ + {{type}} + +
+
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}} +
+
+ {{/annotations}} + + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ {{{annotationform}}} +
+ {{/annotationform}} +
+ {{/annotationmode}} +
+ {{#childentries}} + {{> margic/margic_childentry }} + {{/childentries}} +
\ No newline at end of file diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 0eb20b6..6897762 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -97,106 +97,7 @@ {{/entries.0}} {{#entries}} -
-
-

{{#str}}entry, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} -

- - {{#user}} - {{#canmanageentries}} - {{{userpicture}}} {{^singleuser}}{{/singleuser}} - {{/canmanageentries}} - {{/user}} - - {{#text}} -
- {{{text}}} -
- {{/text}} - {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} -
- {{#stats}} - {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}}
- {{#datediff}} - {{#str}}created, mod_margic, { "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}}
- {{/datediff}} - {{/stats}} - {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{#timemodified}} | {{#str}} lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/timemodified}}
-
- - {{#edittimeends}} -
- {{^edittimehasended}}{{#str}}editingends, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} - {{#edittimehasended}}{{#str}}editingended, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} -
- {{/edittimeends}} - - {{#needsgrading}} -
{{needsgrading}}
- {{/needsgrading}} - - {{#needsregrading}} -
{{needsregrading}}
- {{/needsregrading}} - - {{#canmanageentries}} - {{#gradingform}} -
- {{{gradingform}}} -
- {{/gradingform}} - {{/canmanageentries}} - {{^canmanageentries}} - {{#gradingform}}{{{gradingform}}}{{/gradingform}} - {{/canmanageentries}} - -
- {{#annotationmode}} -
-

{{#str}} annotations, mod_margic {{/str}}

- {{#annotations}} -
-
- - {{type}} - -
-
-
- - {{{userpicturestr}}} - - - {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - -
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} -
- - {{#text}}{{text}}{{/text}} - {{^text}}-{{/text}} - - {{#canbeedited}} - - {{/canbeedited}} -
-
- {{/annotations}} - - {{#annotationform}} -
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} -
- {{{annotationform}}} -
- {{/annotationform}} -
- {{/annotationmode}} -
+ {{> margic/margic_entry }} {{/entries}} {{^entries}} diff --git a/version.php b/version.php index b60a2a1..747a768 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'mod_margic'; $plugin->release = '1.1.3'; // User-friendly version number. -$plugin->version = 2022072400; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022072600; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; From 1771a0bf217229a81a7878d002323c588bcea31c Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 28 Jul 2022 15:53:27 +0200 Subject: [PATCH 19/60] feat (view): layout rework and working on rework of editing entries feature --- classes/local/results.php | 35 ++++++----- classes/output/margic_view.php | 2 +- db/install.xml | 2 +- db/upgrade.php | 4 +- edit.php | 10 ++-- lang/de/margic.php | 5 ++ lang/en/margic.php | 5 ++ locallib.php | 55 +++++++++++++----- styles.css | 79 ++++++++++++++----------- templates/margic_childentry.mustache | 22 ++++++- templates/margic_entry.mustache | 87 +++++++++++++++------------- templates/margic_view.mustache | 16 ++++- 12 files changed, 201 insertions(+), 121 deletions(-) diff --git a/classes/local/results.php b/classes/local/results.php index 5100674..656af54 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -212,6 +212,7 @@ public static function download_entries($context, $course, $margic) { $fields = array(); $fields = array( + get_string('id', 'margic'), get_string('firstname'), get_string('lastname'), get_string('pluginname', 'margic'), @@ -224,8 +225,8 @@ public static function download_entries($context, $course, $margic) { get_string('teacher', 'margic'), get_string('timemarked', 'margic'), get_string('mailed', 'margic'), - get_string('text', 'margic'), - get_string('preventry', 'margic') + get_string('baseentry', 'margic'), + get_string('text', 'margic') ); // Add the headings to our data array. $csv->add_data($fields); @@ -244,7 +245,7 @@ public static function download_entries($context, $course, $margic) { d.teacher AS teacher, to_char(to_timestamp(d.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked, d.mailed AS mailed, - d.preventry AS preventry + d.baseentry AS baseentry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid WHERE d.userid > 0 "; @@ -263,7 +264,7 @@ public static function download_entries($context, $course, $margic) { d.teacher AS teacher, FROM_UNIXTIME(d.timemarked) AS TIMEMARKED, d.mailed AS mailed, - d.preventry AS preventry + d.baseentry AS baseentry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid WHERE d.userid > 0 "; @@ -282,6 +283,7 @@ public static function download_entries($context, $course, $margic) { } $output = array( + $d->entry, $d->firstname, $d->lastname, $d->margic, @@ -294,7 +296,7 @@ public static function download_entries($context, $course, $margic) { $d->teacher, $d->timemarked, $d->mailed, - $d->preventry, + $d->baseentry, format_text($d->text, $d->format, array('para' => false)) ); $csv->add_data($output); @@ -376,8 +378,6 @@ public static function margic_get_editor_and_attachment_options($course, $contex /** * Check for existing rating entry in mdl_rating for the current user. * - * Used in report.php. - * * @param array $ratingoptions An array of current entry data. * @return array $rec An entry was found, so return it for update. */ @@ -467,7 +467,7 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c if ($entry->teacher) { $teacher = $DB->get_record('user', array('id' => $entry->teacher)); - $teacherimage = $OUTPUT->user_picture($teacher, array('courseid' => $course->id, 'link' => true, 'includefullname' => true)); + $teacherimage = $OUTPUT->user_picture($teacher, array('courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 30)); } else { $teacherimage = false; } @@ -481,7 +481,7 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $entry->teacher = $USER->id; } - $feedbackarea .= '

' . get_string('feedback') . '

'; + $feedbackarea .= '
' . get_string('feedback') . '
'; require_once($CFG->dirroot . '/mod/margic/grading_form.php'); @@ -502,20 +502,19 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $data->{'rating_' . $entry->id} = $entry->rating; - $mform = new \mod_margic_grading_form(new \moodle_url('/mod/margic/grade_entry.php', array('id' => $cmid, 'entryid' => $entry->id)), array('courseid' => $course->id, 'margic' => $margic, 'entry' => $entry, 'grades' => $grades, 'teacherimg' => $teacherimage, 'editoroptions' => $editoroptions)); + $mform = new \mod_margic_grading_form(new \moodle_url('/mod/margic/grade_entry.php', array('id' => $cmid, 'entryid' => $entry->id)), + array('courseid' => $course->id, 'margic' => $margic, 'entry' => $entry, 'grades' => $grades, 'teacherimg' => $teacherimage, 'editoroptions' => $editoroptions)); // Set default data. $mform->set_data($data); $feedbackarea .= $mform->render(); } else if ($feedbacktext || ! empty($entry->rating)) { // If user is student and has rating or feedback text. - $feedbackarea .= '

' . get_string('feedback') . '

'; - - $feedbackarea .= '
'; - $feedbackarea .= '' . $teacherimage . ''; - $feedbackarea .= ' - ' . userdate($entry->timemarked) . ''; + $feedbackarea .= '
'; + $feedbackarea .= '
' . get_string('feedback') . ' ' . get_string('from', 'mod_margic') . ' ' . $teacherimage . ' '; + $feedbackarea .= get_string('at', 'mod_margic') . ' ' . userdate($entry->timemarked) . ''; - $feedbackarea .= ''; + $feedbackarea .= ''; if ($margic->assessed > 0) { // Gradebook preference. @@ -537,11 +536,11 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c } $feedbackarea .= ''; - $feedbackarea .= '
'; + $feedbackarea .= ''; // Feedback text. if ($feedbacktext) { - $feedbackarea .= '
' . $feedbacktext; + $feedbackarea .= $feedbacktext; } $feedbackarea .= '
'; diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index dae7e6a..e739b1e 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -179,7 +179,7 @@ public function export_for_template(renderer_base $output) { foreach ($this->entries as $key => $entry) { if ($this->canmanageentries) { // Set user picture for teachers. $this->entries[$key]->user->userpicture = $OUTPUT->user_picture($entry->user, - array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true)); + array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); } // Add feedback area to entry. diff --git a/db/install.xml b/db/install.xml index 4cd788e..35b8c18 100644 --- a/db/install.xml +++ b/db/install.xml @@ -45,7 +45,7 @@ - + diff --git a/db/upgrade.php b/db/upgrade.php index ed36871..faa4b01 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -108,9 +108,9 @@ function xmldb_margic_upgrade($oldversion) { if ($oldversion < 2022072600) { - // Add the preventry field to the margic_entries table. + // Add the baseentry field to the margic_entries table. $table = new xmldb_table('margic_entries'); - $field = new xmldb_field('preventry', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'mailed'); + $field = new xmldb_field('baseentry', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'mailed'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { diff --git a/edit.php b/edit.php index 017d98f..fc69332 100644 --- a/edit.php +++ b/edit.php @@ -91,8 +91,8 @@ $data->id = $cm->id; // Get the single record specified by firstkey. -if (isset($margic->get_entries_with_keys()[$entryid])) { - $entry = $margic->get_entries_with_keys()[$entryid]; +if ($DB->record_exists('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid))) { + $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid)); // Prevent editing of entries not started by this user. if ($entry->userid != $USER->id) { @@ -150,10 +150,10 @@ $newentry->format = 1; if ($fromform->entryid != 0 && $entry != false) { // If existing entry is edited. - if (!isset($entry->preventry)) { - $newentry->preventry = $fromform->entryid; + if (!isset($entry->baseentry)) { + $newentry->baseentry = $fromform->entryid; } else { - $newentry->preventry = $entry->preventry; + $newentry->baseentry = $entry->baseentry; } $newentry->entrycomment = $entry->entrycomment; $newentry->teacher = $entry->teacher; diff --git a/lang/de/margic.php b/lang/de/margic.php index 9a93cd8..b5eecaf 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -262,6 +262,11 @@ $string['prioritychanged'] = 'Reihenfolge geändert'; $string['prioritynotchanged'] = 'Reihenfolge konnte nicht geändert werden'; $string['revision'] = 'Überarbeitung'; +$string['baseentry'] = 'Originaleintrag'; +$string['id'] = 'ID'; +$string['overview'] = 'Übersicht'; +$string['at'] = 'am'; +$string['from'] = 'von'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index ae1821e..becd2b6 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -275,6 +275,11 @@ $string['prioritychanged'] = 'Order changed'; $string['prioritynotchanged'] = 'Order could not be changed'; $string['revision'] = 'Revision'; +$string['baseentry'] = 'Base entry'; +$string['id'] = 'ID'; +$string['overview'] = 'Overview'; +$string['at'] = 'at'; +$string['from'] = 'from'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/locallib.php b/locallib.php index d7a06fb..a8df034 100644 --- a/locallib.php +++ b/locallib.php @@ -256,13 +256,13 @@ function sortannotation($a, $b) { if ($this->mode == 'allentries') { if ($userid && $userid != 0) { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $userid, 'preventry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $userid, 'baseentry' => null), $sortoptions); } else { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'preventry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => null), $sortoptions); } } else if ($this->mode == 'ownentries') { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id, 'preventry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id, 'baseentry' => null), $sortoptions); } $gradingstr = get_string('needsgrading', 'margic'); @@ -278,17 +278,43 @@ function sortannotation($a, $b) { if (!$currentgroups || ($allowedusers && in_array($this->entries[$i]->user, $allowedusers))) { // Get child entries for entry. - $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'preventry' => $entry->id), 'timecreated ASC'); + $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); - $revisionnr = 1; + $revisionnr = count($this->entries[$i]->childentries); foreach ($this->entries[$i]->childentries as $ci => $childentry) { $this->entries[$i]->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager); + $this->entries[$i]->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); $this->entries[$i]->childentries[$ci]->revision = $revisionnr; - $revisionnr += 1; + + if ($ci == array_key_first($this->entries[$i]->childentries)) { + $this->entries[$i]->childentries[$ci]->newestentry = true; + if ($viewinguserid == $childentry->userid) { + $this->entries[$i]->childentries[$ci]->entrycanbeedited = true; + } else { + $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; + } + } else { + $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; + $this->entries[$i]->childentries[$ci]->newestentry = false; + } + + if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { + $this->entries[$i]->entrycanbeedited = true; + } else { + $this->entries[$i]->entrycanbeedited = false; + } + + $revisionnr -= 1; } $this->entries[$i]->childentries = array_values($this->entries[$i]->childentries); + if (empty($this->entries[$i]->childentries)) { + $this->entries[$i]->haschildren = false; + } else { + $this->entries[$i]->haschildren = true; + } + // Get entry stats. $this->entries[$i]->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); @@ -302,7 +328,7 @@ function sortannotation($a, $b) { } // Check if entry can be edited. - if ($viewinguserid == $entry->userid) { + if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { $this->entries[$i]->entrycanbeedited = true; } else { $this->entries[$i]->entrycanbeedited = false; @@ -313,6 +339,12 @@ function sortannotation($a, $b) { } else { unset($this->entries[$i]); } + + // Replace base entry with last child entry for displaying last child entry on top + // if (!empty($this->entries[$i]->childentries)) { + // $baseentry = $this->entries[$i]; + // $this->entries[$i] = $this->entries[$i]->childentries[array_key_last($this->entries[$i]->childentries)]; + // } } } @@ -381,15 +413,6 @@ public function get_entries() { return array_values($this->entries); } - /** - * Returns the entries for the margic instance with intact keys. - * - * @return array action - */ - public function get_entries_with_keys() { - return $this->entries; - } - /** * Returns all annotations for the margic instance. * diff --git a/styles.css b/styles.css index 9d84068..19f93e6 100644 --- a/styles.css +++ b/styles.css @@ -23,11 +23,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -#page-mod-margic-view .feedbackbox { /* Used in renderer.php. */ - width: 85%; - border-collapse: separate; -} - #page-mod-margic-view .entrycontent { /* Used in renderer.php. */ padding: 3px; } @@ -37,16 +32,19 @@ } #page-mod-margic-view .lastedit, +#page-mod-margic-view .needsedit, #page-mod-margic-view .editend { - font-size: 0.7em; margin: 5px; text-align: center; } +#page-mod-margic-view .lastedit { + font-size: 0.7em; +} + #page-mod-margic-view .needsedit, #page-mod-margic-view .editend { color: darkred; - text-align: center; font-weight: bold; } @@ -61,21 +59,13 @@ font-weight: bold; } -#page-mod-margic-view .time { /* Used in renderer.php about line 192. */ - font-size: 0.7em; - font-style: italic; -} - #page-mod-margic-view .grade { /* Used in renderer.php about line 200. */ font-weight: bold; font-style: italic; text-align: right; } -#page-mod-margic-view .entry { - text-align: left; - font-size: 1em; - padding: 10px; +#page-mod-margic-view .entriesheader { margin-bottom: 10px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; @@ -83,24 +73,28 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .feedbackbox .left, -#page-mod-margic-view .feedbackbox .entryheader { /* Used in renderer.php about line 190. */ - background-color: #ddd; -} - -#page-mod-margic-view .feedbackbox { - -moz-border-radius-bottomleft: 15px; - -moz-border-radius-bottomright: 15px; - border-spacing: 0; - margin: 0 auto; +#page-mod-margic-view .entriesheader h4 { + margin-bottom: 5px; + margin-top: 5px; } -#page-mod-margic-view .feedbackbox .side { - -moz-border-radius-bottomleft: 15px; +#page-mod-margic-view .entryfooter { + width: 100%; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; } -#page-mod-margic-view .feedbackbox .entrycontent { - -moz-border-radius-bottomright: 15px; +#page-mod-margic-view .entry { + text-align: left; + font-size: 1em; + padding: 10px; + padding-bottom: 0px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; } #page-mod-margic-view .gradingform, @@ -108,20 +102,36 @@ padding: 10px; text-align: left; font-size: 1em; - margin-top: 5px; - margin-bottom: 5px; + margin: 10px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; } +#page-mod-margic-view .ratingform h5 { + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid rgba(0,0,0,.125); +} + +#page-mod-margic-view .annotationsheader { + margin-bottom: 10px; + border: 1px solid rgba(0, 0, 0, .125); + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; +} + +#page-mod-margic-view .annotationsheader h4 { + margin-bottom: 5px; + margin-top: 5px; +} + #page-mod-margic-view .annotationarea { text-align: left; font-size: 1em; padding: 10px; - margin-bottom: 10px; - margin-left: 10px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; -webkit-border-radius: 5px; @@ -205,7 +215,6 @@ } #page-mod-margic-view .childentrywrapper { - margin-top: -10px; margin-left: 20px; } diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index d6c3a67..af2fb68 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -24,10 +24,18 @@ {{#js}} {{/js}} -
+
-
{{#str}}revision, mod_margic{{/str}} {{revision}} ({{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}) +
{{revision}}. {{#str}}revision, mod_margic{{/str}} + {{#user}}{{#userpicture}} + {{#str}}from, mod_margic {{/str}} + {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} + {{/userpicture}}{{/user}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}}
{{#text}} @@ -36,6 +44,16 @@
{{/text}} {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#stats}} + {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} +
+ {{/stats}} + {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} +
+ {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}} +
{{#annotationmode}}
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index 2e382e7..a8f7665 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -25,17 +25,29 @@ {{/js}}
-
+ + {{#childentries}} + {{> margic/margic_childentry }} + {{/childentries}} +
-

{{#str}}entry, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} -

+
+ {{^haschildren}} + {{#str}}entry, mod_margic{{/str}} + {{/haschildren}} + {{#haschildren}} + {{#str}}baseentry, mod_margic{{/str}} + {{/haschildren}} - {{#user}} - {{#canmanageentries}} - {{{userpicture}}} {{^singleuser}}{{/singleuser}} - {{/canmanageentries}} - {{/user}} + {{#user}}{{#userpicture}} + {{#str}}from, mod_margic {{/str}} + {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} + {{/userpicture}}{{/user}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} +
{{#text}}
@@ -53,36 +65,9 @@ {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}}
- - {{#edittimeends}} -
- {{^edittimehasended}}{{#str}}editingends, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} - {{#edittimehasended}}{{#str}}editingended, mod_margic, {{#userdate}}{{edittimeends}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/str}}: {{/edittimehasended}} -
- {{/edittimeends}} - - {{#needsgrading}} -
{{needsgrading}}
- {{/needsgrading}} - - {{#needsregrading}} -
{{needsregrading}}
- {{/needsregrading}} - - {{#canmanageentries}} - {{#gradingform}} -
- {{{gradingform}}} -
- {{/gradingform}} - {{/canmanageentries}} - {{^canmanageentries}} - {{#gradingform}}{{{gradingform}}}{{/gradingform}} - {{/canmanageentries}}
{{#annotationmode}}
-

{{#str}} annotations, mod_margic {{/str}}

{{#annotations}}
@@ -125,7 +110,31 @@
{{/annotationmode}}
- {{#childentries}} - {{> margic/margic_childentry }} - {{/childentries}} +
\ No newline at end of file diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 6897762..53c8319 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -27,8 +27,7 @@ {{#edittimehasended}}{{#edittimeends}}{{/edittimeends}}{{/edittimehasended}} {{#edittimenotstarted}}{{#edittimestarts}}{{/edittimestarts}}{{/edittimenotstarted}} -{{#canmanageentries}}

{{#str}}entries, mod_margic{{/str}}

{{/canmanageentries}} -{{^canmanageentries}}

{{#str}}myentries, mod_margic{{/str}}

{{/canmanageentries}} +

{{#str}}overview, mod_margic{{/str}}

{{#ratingaggregationmode}}{{#entries.0}} @@ -94,6 +93,19 @@
+
+
+

+ {{#canmanageentries}}{{#str}}entries, mod_margic{{/str}}{{/canmanageentries}} + {{^canmanageentries}}{{#str}}myentries, mod_margic{{/str}}{{/canmanageentries}} +

+
+ {{#annotationmode}} +
+

{{#str}} annotations, mod_margic {{/str}}

+
+ {{/annotationmode}} +
{{/entries.0}} {{#entries}} From 853cdbcbf7fafd6d0ee3938d56098baae533661d Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 28 Jul 2022 18:02:38 +0200 Subject: [PATCH 20/60] feat (view): working on rework of editing entries feature --- annotations.php | 3 +-- edit.php | 44 +++++++++++++++++++++++++++++++------------- lang/de/margic.php | 2 ++ lang/en/margic.php | 2 ++ styles.css | 2 +- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/annotations.php b/annotations.php index 276cdcb..9f1cd3b 100644 --- a/annotations.php +++ b/annotations.php @@ -69,8 +69,7 @@ // Header. $PAGE->set_url('/mod/margic/annotations.php', array('id' => $id)); -$PAGE->navbar->add(get_string('startoreditentry', 'mod_margic')); -$PAGE->set_title(format_string($moduleinstance->name) . ' - ' . get_string('startoreditentry', 'mod_margic')); +$PAGE->set_title(format_string($moduleinstance->name)); $PAGE->set_heading($course->fullname); $urlparams = array('id' => $id, 'annotationmode' => 1); diff --git a/edit.php b/edit.php index fc69332..db43a54 100644 --- a/edit.php +++ b/edit.php @@ -82,9 +82,15 @@ } // Header. +if ($entryid) { + $title = get_string('editentry', 'mod_margic'); +} else { + $title = get_string('addentry', 'mod_margic'); +} + $PAGE->set_url('/mod/margic/edit.php', array('id' => $id)); -$PAGE->navbar->add(get_string('startoreditentry', 'mod_margic')); -$PAGE->set_title(format_string($moduleinstance->name) . ' - ' . get_string('startoreditentry', 'mod_margic')); +$PAGE->navbar->add($title); +$PAGE->set_title(format_string($moduleinstance->name) . ' - ' . $title); $PAGE->set_heading($course->fullname); $data = new stdClass(); @@ -94,8 +100,23 @@ if ($DB->record_exists('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid))) { $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid)); - // Prevent editing of entries not started by this user. - if ($entry->userid != $USER->id) { + $notnewestentry = false; + // Prevent editing of entries that are not the newest version of a base entry or a unedited entry. + if (isset($entry->baseentry)) { // If entry has a base entry check if this entry is the newest childentry. + $otherchildentries = $DB->get_records('margic_entries', array('margic' => $moduleinstance->id, 'baseentry' => $entry->baseentry), 'timecreated DESC'); + + if ($entry->timecreated < $otherchildentries[array_key_first($otherchildentries)]->timecreated) { + $notnewestentry = true; + } + } else { // If this entry has no base entry check if it has childentries and cant therefore be edited. + $childentries = $DB->get_records('margic_entries', array('margic' => $moduleinstance->id, 'baseentry' => $entry->id), 'timecreated DESC'); + + if (!empty($childentries)) { + $notnewestentry = true; + } + } + + if ($entry->userid != $USER->id || $notnewestentry) { // Prevent editing of entries not started by this user or if it is not the newest child entry. // Trigger invalid_access_attempt with redirect to the view page. $params = array( 'objectid' => $id, @@ -154,16 +175,13 @@ $newentry->baseentry = $fromform->entryid; } else { $newentry->baseentry = $entry->baseentry; + + // Update timemodified for base entry. + $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); + $baseentry->timemodified = $fromform->timecreated; + $DB->update_record('margic_entries', $baseentry); } $newentry->entrycomment = $entry->entrycomment; - $newentry->teacher = $entry->teacher; - - $newentry->timecreated = $entry->timecreated; - $newentry->timemarked = $entry->timemarked; - - // Update timemodified for parent entry. - $entry->timemodified = $timenow;; - $DB->update_record('margic_entries', $entry); } if (! $newentry->id = $DB->insert_record("margic_entries", $newentry)) { @@ -213,7 +231,7 @@ $intro = format_module_intro('margic', $moduleinstance, $cm->id); echo $OUTPUT->box($intro); -echo $OUTPUT->heading(get_string('startoreditentry', 'mod_margic'), 3); +echo $OUTPUT->heading($title, 3); // Otherwise fill and print the form. $form->display(); diff --git a/lang/de/margic.php b/lang/de/margic.php index b5eecaf..711c203 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -167,6 +167,8 @@ $string['norating'] = 'Bewertung deaktiviert.'; $string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; $string['startoreditentry'] = 'Eintrag anlegen oder bearbeiten'; +$string['addentry'] = 'Eintrag anlegen'; +$string['editentry'] = 'Eintrag bearbeiten'; $string['editentrynotpossible'] = 'Bearbeiten des Eintrages nicht möglich.'; $string['editdateinfuture'] = 'Das angegebene Erstelldatum des Eintrags liegt in der Zukunft.'; $string['currenttooldest'] = 'Zeige die Einträge vom Aktuellsten zum Ältesten'; diff --git a/lang/en/margic.php b/lang/en/margic.php index becd2b6..89a81a4 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -180,6 +180,8 @@ $string['norating'] = 'Rating disabled.'; $string['viewallmargics'] = 'View all margics in course'; $string['startoreditentry'] = 'Add or edit entry'; +$string['addentry'] = 'Add entry'; +$string['editentry'] = 'Edit entry'; $string['editentrynotpossible'] = 'You can not edit this entry.'; $string['editdateinfuture'] = 'The specified entry date is in the future.'; $string['currenttooldest'] = 'Show entries from current to oldest'; diff --git a/styles.css b/styles.css index 19f93e6..5d5ca3b 100644 --- a/styles.css +++ b/styles.css @@ -112,7 +112,7 @@ #page-mod-margic-view .ratingform h5 { margin-bottom: 10px; padding-bottom: 5px; - border-bottom: 1px solid rgba(0,0,0,.125); + border-bottom: 1px solid rgba(0, 0, 0, .125); } #page-mod-margic-view .annotationsheader { From 1960ef69ad0f98983dd9e6bed45b0458059979e0 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Fri, 29 Jul 2022 15:33:47 +0200 Subject: [PATCH 21/60] feat (edit): working on margic_entry template --- classes/output/margic_entry.php | 190 +++++++++++++++++++++++ classes/output/margic_view.php | 73 +++------ edit.php | 67 +++++++- locallib.php | 256 +++++++++++++++++++++--------- styles.css | 82 +++++----- templates/margic_entry.mustache | 266 ++++++++++++++++++++------------ templates/margic_view.mustache | 2 +- view.php | 10 +- 8 files changed, 673 insertions(+), 273 deletions(-) create mode 100644 classes/output/margic_entry.php diff --git a/classes/output/margic_entry.php b/classes/output/margic_entry.php new file mode 100644 index 0000000..8e6bb56 --- /dev/null +++ b/classes/output/margic_entry.php @@ -0,0 +1,190 @@ +. + +/** + * Class containing data for a margic entry + * + * @package mod_margic + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_margic\output; + +use mod_margic\local\results; +use mod_margic\annotation_form; + +use renderable; +use renderer_base; +use templatable; +use stdClass; + +/** + * Class containing data for a margic entry + * + * @package mod_margic + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class margic_entry implements renderable, templatable { + + /** @var object */ + protected $margic; + /** @var object */ + protected $cm; + /** @var int */ + protected $cmid; + /** @var object */ + protected $context; + /** @var object */ + protected $moduleinstance; + /** @var object */ + protected $entry; + /** @var string */ + protected $entrybgc; + /** @var string */ + protected $entrytextbgc; + /** @var int */ + protected $entryareawidth; + /** @var int */ + protected $annotationareawidth; + /** @var bool */ + protected $caneditentries; + /** @var int */ + protected $edittimestarts; + /** @var bool */ + protected $edittimenotstarted; + /** @var int */ + protected $edittimeends; + /** @var bool */ + protected $edittimehasended; + /** @var bool */ + protected $canmanageentries; + /** @var int */ + protected $course; + /** @var int */ + protected $singleuser; + /** @var bool */ + protected $annotationmode; + /** @var bool */ + protected $canmakeannotations; + /** @var object */ + protected $errortypes; + /** @var bool */ + protected $readonly; + /** @var object */ + protected $grades; + /** @var object */ + protected $currentgroups; + /** @var object */ + protected $allowedusers; + /** @var object */ + protected $strmanager; + /** @var string */ + protected $gradingstr; + /** @var string */ + protected $regradingstr; + /** + * Construct this renderable. + * @param object $margic The margic obj + * @param object $cm The course module + * @param object $context The context + * @param array $moduleinstance The moduleinstance for creating grading form + * @param object $entry The entry + * @param int $entryareawidth Width of the entry area + * @param int $annotationareawidth Width of the annotation area + * @param bool $caneditentries If own entries can be edited + * @param int $edittimestarts Time when entries can be edited + * @param bool $edittimenotstarted If edit time has not started + * @param int $edittimeends Time when entries cant be edited anymore + * @param bool $edittimehasended If edit time has ended + * @param bool $canmanageentries If entries can be managed + * @param int $course The course id for getting the user pictures + * @param int $singleuser If only entries of one user are displayed + * @param bool $annotationmode If annotation mode is set + * @param bool $canmakeannotations If user can make annotations + * @param array $errortypes Array with annotation types for form + * @param bool $readonly If entry and annotations should only be readable + * @param object $grades The grades + * @param object $currentgroups The current groups + * @param object $allowedusers The allowed users + * @param object $strmanager The strmanager + * @param string $gradingstr The gradingstr + * @param string $regradingstr The regradingstr + */ + public function __construct($margic, $cm, $context, $moduleinstance, $entry, $annotationareawidth, + $caneditentries, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, + $course, $singleuser, $annotationmode, $canmakeannotations, $errortypes, $readonly, $grades, $currentgroups, $allowedusers, + $strmanager, $gradingstr, $regradingstr) { + + $this->margic = $margic; + $this->cm = $cm; + $this->cmid = $this->cm->id; + $this->context = $context; + $this->moduleinstance = $moduleinstance; + $this->entry = $entry; + $this->annotationareawidth = $annotationareawidth; + $this->entryareawidth = 100 - $annotationareawidth; + $this->caneditentries = $caneditentries; + $this->edittimestarts = $edittimestarts; + $this->edittimenotstarted = $edittimenotstarted; + $this->edittimeends = $edittimeends; + $this->edittimehasended = $edittimehasended; + $this->canmanageentries = $canmanageentries; + $this->course = $course; + $this->singleuser = $singleuser; + $this->annotationmode = $annotationmode; + $this->canmakeannotations = $canmakeannotations; + $this->errortypes = $errortypes; + $this->readonly = $readonly; + $this->grades = $grades; + $this->currentgroups = $currentgroups; + $this->allowedusers = $allowedusers; + $this->strmanager = $strmanager; + $this->gradingstr = $gradingstr; + $this->regradingstr = $regradingstr; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output Renderer base. + * @return stdClass + */ + public function export_for_template(renderer_base $output) { + $data = new stdClass(); + $data->cmid = $this->cmid; + + $data->entry = $this->margic->prepare_entry($this->entry, $this->strmanager, $this->currentgroups, $this->allowedusers, + $this->gradingstr, $this->regradingstr, $this->readonly, $this->grades, $this->canmanageentries, $this->annotationmode); + + $data->entryareawidth = $this->entryareawidth; + $data->annotationareawidth = $this->annotationareawidth; + $data->caneditentries = $this->caneditentries; + $data->edittimestarts = $this->edittimestarts; + $data->edittimenotstarted = $this->edittimenotstarted; + $data->edittimeends = $this->edittimeends; + $data->edittimehasended = $this->edittimehasended; + $data->canmanageentries = $this->canmanageentries; + $data->singleuser = $this->singleuser; + $data->annotationmode = $this->annotationmode; + $data->canmakeannotations = $this->canmakeannotations; + $data->entrybgc = get_config('mod_margic', 'entrybgc'); + $data->entrytextbgc = get_config('mod_margic', 'entrytextbgc'); + $data->errortypes = $this->errortypes; + $data->readonly = $this->readonly; + return $data; + } +} diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index e739b1e..9b039f6 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -40,6 +40,8 @@ */ class margic_view implements renderable, templatable { + /** @var object */ + protected $margic; /** @var object */ protected $cm; /** @var int */ @@ -96,6 +98,7 @@ class margic_view implements renderable, templatable { protected $errortypes; /** * Construct this renderable. + * @param object $margic The margic obj * @param object $cm The course module * @param object $context The context * @param array $moduleinstance The moduleinstance for creating grading form @@ -123,11 +126,12 @@ class margic_view implements renderable, templatable { * @param bool $canmakeannotations If user can make annotations * @param array $errortypes Array with annotation types for form */ - public function __construct($cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, + public function __construct($margic, $cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, $caneditentries, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, $sesskey, $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, $canmakeannotations, $errortypes) { + $this->margic = $margic; $this->cm = $cm; $this->cmid = $this->cm->id; $this->context = $context; @@ -175,63 +179,31 @@ public function export_for_template(renderer_base $output) { require_once($CFG->dirroot . '/mod/margic/classes/local/results.php'); $grades = make_grades_menu($this->moduleinstance->scale); // For select in grading_form. + $currentgroups = groups_get_activity_group($this->cm, true); // Get a list of the currently allowed groups for this course. + if ($currentgroups) { + $allowedusers = get_users_by_capability($this->context, 'mod/margic:addentries', '', $sort = 'lastname ASC, firstname ASC', '', '', $currentgroups); + } else { + $allowedusers = true; + } - foreach ($this->entries as $key => $entry) { - if ($this->canmanageentries) { // Set user picture for teachers. - $this->entries[$key]->user->userpicture = $OUTPUT->user_picture($entry->user, - array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); - } - - // Add feedback area to entry. - $this->entries[$key]->gradingform = results::margic_return_feedback_area_for_entry($this->cmid, $this->context, $this->course, $this->moduleinstance, - $entry, $grades, $this->canmanageentries); - - // Add annotation form to entry. - if ($this->annotationmode) { - - $mform = new \annotation_form(new \moodle_url('/mod/margic/annotations.php', array('id' => $this->cmid)), array('types' => $this->errortypes)); - - // Set default data. - $mform->set_data(array('id' => $this->cmid, 'entry' => $entry->id)); - - $this->entries[$key]->annotationform = $mform->render(); - - foreach ($this->entries[$key]->annotations as $anr => $annotation) { - $annotater = $DB->get_record('user', array('id' => $annotation->userid)); - $annotaterimage = $OUTPUT->user_picture($annotater, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); - - $this->entries[$key]->annotations[$anr]->userpicturestr = $annotaterimage; - } - - } else { - $this->entries[$key]->annotationform = false; - } - - // Add annotation form to child entries of entry. - foreach ($this->entries[$key]->childentries as $ck => $childentry) { - if ($this->annotationmode) { - - $mform = new \annotation_form(new \moodle_url('/mod/margic/annotations.php', array('id' => $this->cmid)), array('types' => $this->errortypes)); - - // Set default data. - $mform->set_data(array('id' => $this->cmid, 'entry' => $childentry->id)); - - $this->entries[$key]->childentries[$ck]->annotationform = $mform->render(); + $strmanager = get_string_manager(); - foreach ($this->entries[$key]->childentries[$ck]->annotations as $anr => $annotation) { - $annotater = $DB->get_record('user', array('id' => $annotation->userid)); - $annotaterimage = $OUTPUT->user_picture($annotater, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + $gradingstr = get_string('needsgrading', 'margic'); + $regradingstr = get_string('needsregrading', 'margic'); - $this->entries[$key]->childentries[$ck]->annotations[$anr]->userpicturestr = $annotaterimage; - } + $readonly = false; - } else { - $this->entries[$key]->childentries[$ck]->annotationform = false; - } + foreach ($this->entries as $key => $entry) { + if ($entry) { // Set user picture for teachers. + $this->entries[$key]->entry = $OUTPUT->render(new margic_entry($this->margic, $this->cm, $this->context, $this->moduleinstance, + $entry, $this->annotationareawidth, $this->moduleinstance->editall, $this->edittimestarts, $this->edittimenotstarted, + $this->edittimeends, $this->edittimehasended, $this->canmanageentries, $this->course, $this->singleuser, $this->annotationmode, + $this->canmakeannotations, $this->errortypes, $readonly, $grades, $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr)); } } } + $data->entries = $this->entries; $data->sortmode = $this->sortmode; $data->entrybgc = $this->entrybgc; @@ -253,6 +225,7 @@ public function export_for_template(renderer_base $output) { $data->entriescount = $this->entriescount; $data->annotationmode = $this->annotationmode; $data->canmakeannotations = $this->canmakeannotations; + return $data; } } diff --git a/edit.php b/edit.php index db43a54..f76b0ef 100644 --- a/edit.php +++ b/edit.php @@ -25,6 +25,7 @@ use mod_margic\local\results; use \mod_margic\event\invalid_access_attempt; use core\output\notification; +use mod_margic\output\margic_entry; require_once("../../config.php"); require_once('./edit_form.php'); @@ -134,6 +135,10 @@ $data->timecreated = $entry->timecreated; $data->text = $entry->text; $data->textformat = $entry->format; + + $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', + array('annotations' => $margic->get_annotations(), + 'canmakeannotations' => false)); } else { $entry = false; @@ -175,12 +180,13 @@ $newentry->baseentry = $fromform->entryid; } else { $newentry->baseentry = $entry->baseentry; - - // Update timemodified for base entry. - $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); - $baseentry->timemodified = $fromform->timecreated; - $DB->update_record('margic_entries', $baseentry); } + + // Update timemodified for base entry. + $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry)); + $baseentry->timemodified = $fromform->timecreated; + $DB->update_record('margic_entries', $baseentry); + $newentry->entrycomment = $entry->entrycomment; } @@ -233,6 +239,57 @@ echo $OUTPUT->heading($title, 3); +// Calculate if edit time has started. +$timenow = time(); +if (!$moduleinstance->timeopen) { + $edittimenotstarted = false; + $edittimestarts = false; +} else if ($moduleinstance->timeopen && $timenow >= $moduleinstance->timeopen) { + $edittimenotstarted = false; + $edittimestarts = $moduleinstance->timeopen; +} else if ($moduleinstance->timeopen && $timenow < $moduleinstance->timeopen) { + $edittimenotstarted = true; + $edittimestarts = $moduleinstance->timeopen; +} + +// Calculate if edit time has ended. +if (!$moduleinstance->timeclose) { + $edittimehasended = false; + $edittimeends = false; +} else if ($moduleinstance->timeclose && $timenow < $moduleinstance->timeclose) { + $edittimehasended = false; + $edittimeends = $moduleinstance->timeclose; +} else if ($moduleinstance->timeclose && $timenow >= $moduleinstance->timeclose) { + $edittimehasended = true; + $edittimeends = $moduleinstance->timeclose; +} + +$grades = make_grades_menu($moduleinstance->scale); // For select in grading_form. + +$currentgroups = groups_get_activity_group($cm, true); // Get a list of the currently allowed groups for this course. + +if ($currentgroups) { + $allowedusers = get_users_by_capability($context, 'mod/margic:addentries', '', $sort = 'lastname ASC, firstname ASC', '', '', $currentgroups); +} else { + $allowedusers = true; +} + +$strmanager = get_string_manager(); + +$gradingstr = get_string('needsgrading', 'margic'); +$regradingstr = get_string('needsregrading', 'margic'); + +if ($entry->baseentry) { // If edited entry is child entry get base entry for rendering. + $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); +} + +$page = new margic_entry($margic, $cm, $context, $moduleinstance, $entry, $margic->get_annotationarea_width(), + $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, + has_capability('mod/margic:manageentries', $context), $course, false, true, false, false, true, $grades, + $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr); + +echo $OUTPUT->render($page); + // Otherwise fill and print the form. $form->display(); diff --git a/locallib.php b/locallib.php index a8df034..3ea3104 100644 --- a/locallib.php +++ b/locallib.php @@ -273,79 +273,79 @@ function sortannotation($a, $b) { $strmanager = get_string_manager(); // Prepare entries. - foreach ($this->entries as $i => $entry) { - $this->entries[$i]->user = $DB->get_record('user', array('id' => $entry->userid)); - - if (!$currentgroups || ($allowedusers && in_array($this->entries[$i]->user, $allowedusers))) { - // Get child entries for entry. - $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); - - $revisionnr = count($this->entries[$i]->childentries); - foreach ($this->entries[$i]->childentries as $ci => $childentry) { - $this->entries[$i]->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager); - $this->entries[$i]->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); - $this->entries[$i]->childentries[$ci]->revision = $revisionnr; - - if ($ci == array_key_first($this->entries[$i]->childentries)) { - $this->entries[$i]->childentries[$ci]->newestentry = true; - if ($viewinguserid == $childentry->userid) { - $this->entries[$i]->childentries[$ci]->entrycanbeedited = true; - } else { - $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; - } - } else { - $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; - $this->entries[$i]->childentries[$ci]->newestentry = false; - } - - if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { - $this->entries[$i]->entrycanbeedited = true; - } else { - $this->entries[$i]->entrycanbeedited = false; - } - - $revisionnr -= 1; - } - - $this->entries[$i]->childentries = array_values($this->entries[$i]->childentries); - - if (empty($this->entries[$i]->childentries)) { - $this->entries[$i]->haschildren = false; - } else { - $this->entries[$i]->haschildren = true; - } - - // Get entry stats. - $this->entries[$i]->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); - - // Check entry grading. - if (!empty($entry->timecreated) && empty($entry->timemarked)) { - $this->entries[$i]->needsgrading = $gradingstr; - } else if (!empty($entry->timemodified) && !empty($entry->timemarked) && $entry->timemodified > $entry->timemarked) { - $this->entries[$i]->needsregrading = $regradingstr; - } else { - $this->entries[$i]->needsregrading = false; - } - - // Check if entry can be edited. - if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { - $this->entries[$i]->entrycanbeedited = true; - } else { - $this->entries[$i]->entrycanbeedited = false; - } - - // Prepare entry annotations. - $this->entries[$i] = $this->prepare_entry_annotations($entry, $strmanager); - } else { - unset($this->entries[$i]); - } + // foreach ($this->entries as $i => $entry) { + // $this->entries[$i]->user = $DB->get_record('user', array('id' => $entry->userid)); + + // if (!$currentgroups || ($allowedusers && in_array($this->entries[$i]->user, $allowedusers))) { + // // Get child entries for entry. + // $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); + + // $revisionnr = count($this->entries[$i]->childentries); + // foreach ($this->entries[$i]->childentries as $ci => $childentry) { + // $this->entries[$i]->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager); + // $this->entries[$i]->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); + // $this->entries[$i]->childentries[$ci]->revision = $revisionnr; + + // if ($ci == array_key_first($this->entries[$i]->childentries)) { + // $this->entries[$i]->childentries[$ci]->newestentry = true; + // if ($viewinguserid == $childentry->userid) { + // $this->entries[$i]->childentries[$ci]->entrycanbeedited = true; + // } else { + // $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; + // } + // } else { + // $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; + // $this->entries[$i]->childentries[$ci]->newestentry = false; + // } + + // if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { + // $this->entries[$i]->entrycanbeedited = true; + // } else { + // $this->entries[$i]->entrycanbeedited = false; + // } + + // $revisionnr -= 1; + // } + + // $this->entries[$i]->childentries = array_values($this->entries[$i]->childentries); + + // if (empty($this->entries[$i]->childentries)) { + // $this->entries[$i]->haschildren = false; + // } else { + // $this->entries[$i]->haschildren = true; + // } + + // // Get entry stats. + // $this->entries[$i]->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); + + // // Check entry grading. + // if (!empty($entry->timecreated) && empty($entry->timemarked)) { + // $this->entries[$i]->needsgrading = $gradingstr; + // } else if (!empty($entry->timemodified) && !empty($entry->timemarked) && $entry->timemodified > $entry->timemarked) { + // $this->entries[$i]->needsregrading = $regradingstr; + // } else { + // $this->entries[$i]->needsregrading = false; + // } + + // // Check if entry can be edited. + // if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { + // $this->entries[$i]->entrycanbeedited = true; + // } else { + // $this->entries[$i]->entrycanbeedited = false; + // } + + // // Prepare entry annotations. + // $this->entries[$i] = $this->prepare_entry_annotations($entry, $strmanager); + // } else { + // unset($this->entries[$i]); + // } // Replace base entry with last child entry for displaying last child entry on top // if (!empty($this->entries[$i]->childentries)) { // $baseentry = $this->entries[$i]; // $this->entries[$i] = $this->entries[$i]->childentries[array_key_last($this->entries[$i]->childentries)]; // } - } + //} } /** @@ -424,6 +424,21 @@ public function get_annotations() { return $this->annotations; } + /** + * Returns the width of the annotation area. + * + * @return int annotationareawidth + */ + public function get_annotationarea_width() { + if (isset($this->instance->annotationareawidth)) { + $annotationareawidth = $this->instance->annotationareawidth; + } else { + $annotationareawidth = get_config('mod_margic', 'annotationareawidth'); + } + + return $annotationareawidth; + } + /** * Returns all errortypes. * @@ -598,8 +613,89 @@ private function search_dom_node(DOMNode $domnode, &$position = 0) { } } - private function prepare_entry_annotations($entry, $strmanager) { - global $DB, $USER; + public function prepare_entry($entry, $strmanager, $currentgroups, $allowedusers, $gradingstr, $regradingstr, $readonly, $grades, $canmanageentries, $annotationmode) { + global $DB, $USER, $CFG, $OUTPUT; + + $entry->user = $DB->get_record('user', array('id' => $entry->userid)); + + if (!$currentgroups || ($allowedusers && in_array($entry->user, $allowedusers))) { + // Get child entries for entry. + $entry->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); + + $revisionnr = count($entry->childentries); + foreach ($entry->childentries as $ci => $childentry) { + $entry->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager, $annotationmode, $readonly); + $entry->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); + $entry->childentries[$ci]->revision = $revisionnr; + + if ($ci == array_key_first($entry->childentries)) { + $entry->childentries[$ci]->newestentry = true; + if ($USER->id == $childentry->userid && !$readonly) { + $entry->childentries[$ci]->entrycanbeedited = true; + } else { + $entry->childentries[$ci]->entrycanbeedited = false; + } + } else { + $entry->childentries[$ci]->entrycanbeedited = false; + $entry->childentries[$ci]->newestentry = false; + } + + if ($USER->id == $entry->userid && empty($entry->childentries) && !$readonly) { + $entry->entrycanbeedited = true; + } else { + $entry->entrycanbeedited = false; + } + + $revisionnr -= 1; + } + + $entry->childentries = array_values($entry->childentries); + + if (empty($entry->childentries)) { + $entry->haschildren = false; + } else { + $entry->haschildren = true; + } + + // Get entry stats. + $entry->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); + + // Check entry grading. + if (!empty($entry->timecreated) && empty($entry->timemarked)) { + $entry->needsgrading = $gradingstr; + } else if (!empty($entry->timemodified) && !empty($entry->timemarked) && $entry->timemodified > $entry->timemarked) { + $entry->needsregrading = $regradingstr; + } else { + $entry->needsregrading = false; + } + + // Check if entry can be edited. + if ($USER->id == $entry->userid && empty($entry->childentries)) { + $entry->entrycanbeedited = true; + } else { + $entry->entrycanbeedited = false; + } + + require_once($CFG->dirroot . '/mod/margic/annotation_form.php'); + require_once($CFG->dirroot . '/mod/margic/classes/local/results.php'); + + $entry->user->userpicture = $OUTPUT->user_picture($entry->user, + array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); + + // Add feedback area to entry. + $entry->gradingform = results::margic_return_feedback_area_for_entry($this->cm->id, $this->context, $this->course, $this->instance, + $entry, $grades, $canmanageentries); + + $entry = $this->prepare_entry_annotations($entry, $strmanager, $annotationmode, $readonly); + + return $entry; + } else { + return false; + } + } + + private function prepare_entry_annotations($entry, $strmanager, $annotationmode = false, $readonly = false) { + global $DB, $USER, $CFG, $OUTPUT; // Index entry for annotation sorting. $position = 0; @@ -647,6 +743,16 @@ private function prepare_entry_annotations($entry, $strmanager) { $entry->annotations[$key]->canbeedited = false; } + if ($annotationmode) { + // Add annotater images to annotations. + $annotater = $DB->get_record('user', array('id' => $annotation->userid)); + $annotaterimage = $OUTPUT->user_picture($annotater, array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + $entry->annotations[$key]->userpicturestr = $annotaterimage; + + } else { + $entry->annotationform = false; + } + // Get position of startcontainer. $xpath = new DOMXpath($doc); $nodelist = $xpath->query('/' . $annotation->startcontainer); @@ -682,6 +788,18 @@ private function prepare_entry_annotations($entry, $strmanager) { // Reset nodepositions with empty array for next entry. $this->nodepositions = array(); + if ($annotationmode) { + // Add annotation form. + if (!$readonly) { + require_once($CFG->dirroot . '/mod/margic/annotation_form.php'); + $mform = new annotation_form(new moodle_url('/mod/margic/annotations.php', array('id' => $this->cm->id)), array('types' => $this->get_errortypes_for_form())); + // Set default data. + $mform->set_data(array('id' => $this->cm->id, 'entry' => $entry->id)); + + $entry->annotationform = $mform->render(); + } + } + return $entry; } } diff --git a/styles.css b/styles.css index 5d5ca3b..b905dc7 100644 --- a/styles.css +++ b/styles.css @@ -23,49 +23,49 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ -#page-mod-margic-view .entrycontent { /* Used in renderer.php. */ +.path-mod-margic .entrycontent { /* Used in renderer.php. */ padding: 3px; } -#page-mod-margic-view .picture { /* Used in renderer.php with left. */ +.path-mod-margic .picture { /* Used in renderer.php with left. */ width: 35px; } -#page-mod-margic-view .lastedit, -#page-mod-margic-view .needsedit, -#page-mod-margic-view .editend { +.path-mod-margic .lastedit, +.path-mod-margic .needsedit, +.path-mod-margic .editend { margin: 5px; text-align: center; } -#page-mod-margic-view .lastedit { +.path-mod-margic .lastedit { font-size: 0.7em; } -#page-mod-margic-view .needsedit, -#page-mod-margic-view .editend { +.path-mod-margic .needsedit, +.path-mod-margic .editend { color: darkred; font-weight: bold; } -#page-mod-margic-view .needsedit span, -#page-mod-margic-view .editend span { +.path-mod-margic .needsedit span, +.path-mod-margic .editend span { background: white; padding: 2px; } -#page-mod-margic-view .author { /* Used in renderer.php about line 191. */ +.path-mod-margic .author { /* Used in renderer.php about line 191. */ font-size: 1em; font-weight: bold; } -#page-mod-margic-view .grade { /* Used in renderer.php about line 200. */ +.path-mod-margic .grade { /* Used in renderer.php about line 200. */ font-weight: bold; font-style: italic; text-align: right; } -#page-mod-margic-view .entriesheader { +.path-mod-margic .entriesheader { margin-bottom: 10px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; @@ -73,12 +73,12 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .entriesheader h4 { +.path-mod-margic .entriesheader h4 { margin-bottom: 5px; margin-top: 5px; } -#page-mod-margic-view .entryfooter { +.path-mod-margic .entryfooter { width: 100%; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; @@ -86,7 +86,7 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .entry { +.path-mod-margic .entry { text-align: left; font-size: 1em; padding: 10px; @@ -97,8 +97,8 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .gradingform, -#page-mod-margic-view .ratingform { +.path-mod-margic .gradingform, +.path-mod-margic .ratingform { padding: 10px; text-align: left; font-size: 1em; @@ -109,13 +109,13 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .ratingform h5 { +.path-mod-margic .ratingform h5 { margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid rgba(0, 0, 0, .125); } -#page-mod-margic-view .annotationsheader { +.path-mod-margic .annotationsheader { margin-bottom: 10px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; @@ -123,12 +123,12 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .annotationsheader h4 { +.path-mod-margic .annotationsheader h4 { margin-bottom: 5px; margin-top: 5px; } -#page-mod-margic-view .annotationarea { +.path-mod-margic .annotationarea { text-align: left; font-size: 1em; padding: 10px; @@ -138,30 +138,30 @@ -moz-border-radius: 5px; } -#page-mod-margic-view .annotationarea .annotatedtextpreview:hover { +.path-mod-margic .annotationarea .annotatedtextpreview:hover { background-color: lightblue; cursor: pointer; } -#page-mod-margic-view .annotated, -#page-mod-margic-view .annotated_temp { +.path-mod-margic .annotated, +.path-mod-margic .annotated_temp { background-color: yellow; cursor: pointer; } -#page-mod-margic-view .hovered .annotated, -#page-mod-margic-view .hovered .annotated_temp { +.path-mod-margic .hovered .annotated, +.path-mod-margic .hovered .annotated_temp { background: none; } -#page-mod-margic-view .annotated:hover, -#page-mod-margic-view .annotated_temp:hover, -#page-mod-margic-view .hovered, -#page-mod-margic-view .errortypeheader .hovered { +.path-mod-margic .annotated:hover, +.path-mod-margic .annotated_temp:hover, +.path-mod-margic .hovered, +.path-mod-margic .errortypeheader .hovered { background-color: lightblue !important; } -#page-mod-margic-view .annotation-box { +.path-mod-margic .annotation-box { background-color: white; border: 1px solid #dbdbdb; border-radius: 2px; @@ -170,34 +170,34 @@ box-shadow: 0 1px 1px rgba(0, 0, 0, .1); } -#page-mod-margic-view .annotation-box:hover { +.path-mod-margic .annotation-box:hover { box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, .15); } -#page-mod-margic-view #id_submitbutton { +.path-mod-margic #id_submitbutton { margin-right: 5px; } -#page-mod-margic-view textarea { +.path-mod-margic textarea { width: 100%; } -#page-mod-margic-annotations_summary th { +.path-mod-margic-annotations_summary th { min-width: 135px; } -#page-mod-margic-view .annotatedtextpreviewdiv { +.path-mod-margic .annotatedtextpreviewdiv { margin-top: 5px; margin-bottom: 5px; } -#page-mod-margic-view .annotationauthor { +.path-mod-margic .annotationauthor { padding-top: 5px; padding-bottom: 5px; margin-top: 10px; } -#page-mod-margic-view .annotatedtextpreview { +.path-mod-margic .annotatedtextpreview { border-left: 5px solid yellow; padding-left: 5px; background-color: white; @@ -205,7 +205,7 @@ width: 100%; } -#page-mod-margic-view .margic-btn-round-small { +.path-mod-margic .margic-btn-round-small { width: 1.6rem; height: 1.6rem; border-radius: 50%; @@ -214,7 +214,7 @@ margin-bottom: 5px; } -#page-mod-margic-view .childentrywrapper { +.path-mod-margic .childentrywrapper { margin-left: 20px; } diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index a8f7665..121720a 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -19,122 +19,190 @@ @template margic/margic_entry Template for single entry. + + Context variables required for this template: + * + + Example context (json): + { + "cmid": 145, + "childentries": [ + { + "id": 1, + ... + }, + { + "id": 2, + ... + } + ], + "haschildren": true, + "annotationmode": false, + "entrybgc": FFFFFF, + "entryareawidth" 40%, + "canmanageentries": false, + "edittimeends": false, + "edittimehasended": false, + "caneditentries": true, + "entrycanbeedited": true + "user": ... + "userpicture": ... + "singleuser": false, + "timecreated": 1658849477, + "timemodified": 0, + "needsgrading": true, + "needsregrading": false, + "stats": { + "words": 10, + "chars": 20, + "spaces": 4, + "datediff": [ + { + "d": 10, + "chars": 20, + "spaces": 4, + "datediff": + }, + { + "name": "Quiz 1", + "instructorchoiceacceptgrades": 1, + "grade_modgrade_point": 20.5 + } + ], + } + ], + "gradingform": "
string with grading form
", + "annotations": [ + { + "id": 1, + ... + }, + { + "id": 2, + ... + } + ], + } }} {{#js}} {{/js}} -
+{{#entry}} +
- {{#childentries}} - {{> margic/margic_childentry }} - {{/childentries}} -
-
-
- {{^haschildren}} - {{#str}}entry, mod_margic{{/str}} - {{/haschildren}} - {{#haschildren}} - {{#str}}baseentry, mod_margic{{/str}} - {{/haschildren}} + {{#childentries}} + {{> margic/margic_childentry }} + {{/childentries}} +
+
+
+ {{^haschildren}} + {{#str}}entry, mod_margic{{/str}} + {{/haschildren}} + {{#haschildren}} + {{#str}}baseentry, mod_margic{{/str}} + {{/haschildren}} - {{#user}}{{#userpicture}} - {{#str}}from, mod_margic {{/str}} - {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} - {{/userpicture}}{{/user}} - {{#str}}at, mod_margic {{/str}} - {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} - {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} - {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} -
+ {{#user}}{{#userpicture}} + {{#str}}from, mod_margic {{/str}} + {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} + {{/userpicture}}{{/user}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{^readonly}} + {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} + {{/readonly}} +
- {{#text}} -
- {{{text}}} + {{#text}} +
+ {{{text}}} +
+ {{/text}} + {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#stats}} + {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} +
+ {{/stats}} + {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} + + {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}}
- {{/text}} - {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} -
- {{#stats}} - {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} -
- {{/stats}} - {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} - - {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}}
-
- {{#annotationmode}} -
- {{#annotations}} -
-
- - {{type}} - -
-
-
- - {{{userpicturestr}}} - - - {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - + {{#annotationmode}} +
+ {{#annotations}} +
+
+ + {{type}} +
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}}
- - {{#text}}{{text}}{{/text}} - {{^text}}-{{/text}} - - {{#canbeedited}} - - {{/canbeedited}}
-
- {{/annotations}} + {{/annotations}} - {{#annotationform}} -
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ {{{annotationform}}}
- {{{annotationform}}} -
- {{/annotationform}} -
- {{/annotationmode}} -
- +
-
\ No newline at end of file +{{/entry}} \ No newline at end of file diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 53c8319..28da3cc 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -109,7 +109,7 @@ {{/entries.0}} {{#entries}} - {{> margic/margic_entry }} + {{{entry}}} {{/entries}} {{^entries}} diff --git a/view.php b/view.php index a82af48..1e5d908 100644 --- a/view.php +++ b/view.php @@ -183,19 +183,13 @@ $edittimeends = $moduleinstance->timeclose; } -// Get width of annotation area. -if (isset($moduleinstance->annotationareawidth)) { - $annotationareawidth = $moduleinstance->annotationareawidth; -} else { - $annotationareawidth = get_config('mod_margic', 'annotationareawidth'); -} // Handle groups. echo groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/margic/view.php?id=$id"); // Output page. -$page = new margic_view($cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), $margic->get_sortmode(), - get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $annotationareawidth, +$page = new margic_view($margic, $cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), $margic->get_sortmode(), + get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $margic->get_annotationarea_width(), $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), $annotationmode, $canmakeannotations, $margic->get_errortypes_for_form()); From 98046ca3f095efb11d34998b9426c16a541b421b Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Fri, 29 Jul 2022 17:39:19 +0200 Subject: [PATCH 22/60] feat (edit): now only using margic_entry template --- classes/local/results.php | 38 +++++++ edit.php | 70 +++++-------- lang/de/margic.php | 5 +- lang/en/margic.php | 5 +- locallib.php | 104 +------------------ templates/margic_childentry.mustache | 14 +-- templates/margic_entry.mustache | 150 ++++++++++++++------------- templates/margic_view.mustache | 7 +- view.php | 40 ++----- 9 files changed, 168 insertions(+), 265 deletions(-) diff --git a/classes/local/results.php b/classes/local/results.php index 656af54..a278a4f 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -375,6 +375,44 @@ public static function margic_get_editor_and_attachment_options($course, $contex ); } + /** + * Return the edit time options for a margic. + * + * @param stdClass $moduleinstance The margic module instance. + * @return array $editoroptions Array containing the editor and attachment options. + * @return array $attachmentoptions Array containing the editor and attachment options. + */ + public static function margic_get_edittime_options($moduleinstance) { + $edittimes = new stdClass(); + + // Calculate if edit time has started. + $timenow = time(); + if (!$moduleinstance->timeopen) { + $edittimes->edittimenotstarted = false; + $edittimes->edittimestarts = false; + } else if ($moduleinstance->timeopen && $timenow >= $moduleinstance->timeopen) { + $edittimes->edittimenotstarted = false; + $edittimes->edittimestarts = $moduleinstance->timeopen; + } else if ($moduleinstance->timeopen && $timenow < $moduleinstance->timeopen) { + $edittimes->edittimenotstarted = true; + $edittimes->edittimestarts = $moduleinstance->timeopen; + } + + // Calculate if edit time has ended. + if (!$moduleinstance->timeclose) { + $edittimes->edittimehasended = false; + $edittimes->edittimeends = false; + } else if ($moduleinstance->timeclose && $timenow < $moduleinstance->timeclose) { + $edittimes->edittimehasended = false; + $edittimes->edittimeends = $moduleinstance->timeclose; + } else if ($moduleinstance->timeclose && $timenow >= $moduleinstance->timeclose) { + $edittimes->edittimehasended = true; + $edittimes->edittimeends = $moduleinstance->timeclose; + } + + return $edittimes; + } + /** * Check for existing rating entry in mdl_rating for the current user. * diff --git a/edit.php b/edit.php index f76b0ef..df3922c 100644 --- a/edit.php +++ b/edit.php @@ -86,7 +86,7 @@ if ($entryid) { $title = get_string('editentry', 'mod_margic'); } else { - $title = get_string('addentry', 'mod_margic'); + $title = get_string('addnewentry', 'mod_margic'); } $PAGE->set_url('/mod/margic/edit.php', array('id' => $id)); @@ -170,7 +170,7 @@ $newentry->userid = $USER->id; $newentry->timecreated = $fromform->timecreated; - $newentry->timemodified = 0; + $newentry->timemodified = $fromform->timecreated; $newentry->text = ''; $newentry->format = 1; @@ -239,58 +239,38 @@ echo $OUTPUT->heading($title, 3); -// Calculate if edit time has started. -$timenow = time(); -if (!$moduleinstance->timeopen) { - $edittimenotstarted = false; - $edittimestarts = false; -} else if ($moduleinstance->timeopen && $timenow >= $moduleinstance->timeopen) { - $edittimenotstarted = false; - $edittimestarts = $moduleinstance->timeopen; -} else if ($moduleinstance->timeopen && $timenow < $moduleinstance->timeopen) { - $edittimenotstarted = true; - $edittimestarts = $moduleinstance->timeopen; -} - -// Calculate if edit time has ended. -if (!$moduleinstance->timeclose) { - $edittimehasended = false; - $edittimeends = false; -} else if ($moduleinstance->timeclose && $timenow < $moduleinstance->timeclose) { - $edittimehasended = false; - $edittimeends = $moduleinstance->timeclose; -} else if ($moduleinstance->timeclose && $timenow >= $moduleinstance->timeclose) { - $edittimehasended = true; - $edittimeends = $moduleinstance->timeclose; -} +// If existing entry is edited render entry. +if ($entry) { + $edittimes = results::margic_get_edittime_options($moduleinstance); -$grades = make_grades_menu($moduleinstance->scale); // For select in grading_form. + $grades = make_grades_menu($moduleinstance->scale); // For select in grading_form. -$currentgroups = groups_get_activity_group($cm, true); // Get a list of the currently allowed groups for this course. + $currentgroups = groups_get_activity_group($cm, true); // Get a list of the currently allowed groups for this course. -if ($currentgroups) { - $allowedusers = get_users_by_capability($context, 'mod/margic:addentries', '', $sort = 'lastname ASC, firstname ASC', '', '', $currentgroups); -} else { - $allowedusers = true; -} + if ($currentgroups) { + $allowedusers = get_users_by_capability($context, 'mod/margic:addentries', '', $sort = 'lastname ASC, firstname ASC', '', '', $currentgroups); + } else { + $allowedusers = true; + } -$strmanager = get_string_manager(); + $strmanager = get_string_manager(); -$gradingstr = get_string('needsgrading', 'margic'); -$regradingstr = get_string('needsregrading', 'margic'); + $gradingstr = get_string('needsgrading', 'margic'); + $regradingstr = get_string('needsregrading', 'margic'); -if ($entry->baseentry) { // If edited entry is child entry get base entry for rendering. - $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); -} + if ($entry->baseentry) { // If edited entry is child entry get base entry for rendering. + $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); + } -$page = new margic_entry($margic, $cm, $context, $moduleinstance, $entry, $margic->get_annotationarea_width(), - $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, - has_capability('mod/margic:manageentries', $context), $course, false, true, false, false, true, $grades, - $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr); + $page = new margic_entry($margic, $cm, $context, $moduleinstance, $entry, $margic->get_annotationarea_width(), + $moduleinstance->editall, $edittimes->edittimestarts, $edittimes->edittimenotstarted, $edittimes->edittimeends, + $edittimes->edittimehasended, has_capability('mod/margic:manageentries', $context), $course, false, true, false, + false, true, $grades, $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr); -echo $OUTPUT->render($page); + echo $OUTPUT->render($page); +} -// Otherwise fill and print the form. +// Display the form for editing the entry. $form->display(); echo $OUTPUT->footer(); diff --git a/lang/de/margic.php b/lang/de/margic.php index 711c203..7b9229b 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -167,7 +167,7 @@ $string['norating'] = 'Bewertung deaktiviert.'; $string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; $string['startoreditentry'] = 'Eintrag anlegen oder bearbeiten'; -$string['addentry'] = 'Eintrag anlegen'; +$string['addnewentry'] = 'Neuen Eintrag anlegen'; $string['editentry'] = 'Eintrag bearbeiten'; $string['editentrynotpossible'] = 'Bearbeiten des Eintrages nicht möglich.'; $string['editdateinfuture'] = 'Das angegebene Erstelldatum des Eintrags liegt in der Zukunft.'; @@ -175,13 +175,11 @@ $string['oldesttocurrent'] = 'Zeige die Einträge vom Ältesten zum Aktuellsten'; $string['lowestgradetohighest'] = 'Zeige die Einträge vom am niedrigsten Bewerteten zum Höchsten'; $string['highestgradetolowest'] = 'Zeige die Einträge vom am höchsten Bewerteten zum Niedrigsten'; -$string['lastmodified'] = 'Zeige die zuletzt geänderten Einträge'; $string['sorting'] = 'Sortierung'; $string['currententry'] = 'Aktuelle Einträge'; $string['oldestentry'] = 'Älteste Einträge'; $string['lowestgradeentry'] = 'Am niedrigsten bewertete Einträge'; $string['highestgradeentry'] = 'Am höchsten bewertete Beiträge'; -$string['latestmodifiedentry'] = 'Zuletzt geänderte Einträge'; $string['viewallentries'] = 'Alle Einträge ansehen'; $string['grammar_verb'] = 'Grammatik: Verbform'; @@ -269,6 +267,7 @@ $string['overview'] = 'Übersicht'; $string['at'] = 'am'; $string['from'] = 'von'; +$string['toggleolderversions'] = 'Ältere Versionen ein- oder ausblenden'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 89a81a4..a1865d9 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -180,7 +180,7 @@ $string['norating'] = 'Rating disabled.'; $string['viewallmargics'] = 'View all margics in course'; $string['startoreditentry'] = 'Add or edit entry'; -$string['addentry'] = 'Add entry'; +$string['addnewentry'] = 'Add new entry'; $string['editentry'] = 'Edit entry'; $string['editentrynotpossible'] = 'You can not edit this entry.'; $string['editdateinfuture'] = 'The specified entry date is in the future.'; @@ -188,13 +188,11 @@ $string['oldesttocurrent'] = 'Show entries from oldest to current'; $string['lowestgradetohighest'] = 'Show entries from the lowest rated to the highest one'; $string['highestgradetolowest'] = 'Show entries from the highest rated to the lowest one'; -$string['latestmodified'] = 'Show the last modified entries'; $string['sorting'] = 'Sorting'; $string['currententry'] = 'Current entries'; $string['oldestentry'] = 'Oldest entries'; $string['lowestgradeentry'] = 'Lowest rated entries'; $string['highestgradeentry'] = 'Highest rated entries'; -$string['latestmodifiedentry'] = 'Last modified entries'; $string['viewallentries'] = 'View all entries'; $string['grammar_verb'] = 'Grammar: Verb form'; @@ -282,6 +280,7 @@ $string['overview'] = 'Overview'; $string['at'] = 'at'; $string['from'] = 'from'; +$string['toggleolderversions'] = 'Toggle older versions of the entry'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/locallib.php b/locallib.php index 3ea3104..bf248c9 100644 --- a/locallib.php +++ b/locallib.php @@ -170,9 +170,6 @@ function sortannotation($a, $b) { case 'highestgradetolowest': set_user_preference('sortoption['.$id.']', 4); break; - case 'latestmodified': - set_user_preference('sortoption['.$id.']', 5); - break; default: if (!get_user_preferences('sortoption['.$id.']')) { set_user_preference('sortoption['.$id.']', 1); @@ -182,11 +179,11 @@ function sortannotation($a, $b) { switch (get_user_preferences('sortoption['.$id.']')) { case 1: $this->sortmode = get_string('currententry', 'mod_margic'); - $sortoptions = 'timecreated DESC'; + $sortoptions = 'timemodified DESC'; break; case 2: $this->sortmode = get_string('oldestentry', 'mod_margic'); - $sortoptions = 'timecreated ASC'; + $sortoptions = 'timemodified ASC'; break; case 3: $this->sortmode = get_string('lowestgradeentry', 'mod_margic'); @@ -196,13 +193,9 @@ function sortannotation($a, $b) { $this->sortmode = get_string('highestgradeentry', 'mod_margic'); $sortoptions = 'rating DESC, timemodified DESC'; break; - case 5: - $this->sortmode = get_string('latestmodifiedentry', 'mod_margic'); - $sortoptions = 'timemodified DESC, timecreated DESC'; - break; default: $this->sortmode = get_string('currententry', 'mod_margic'); - $sortoptions = 'timecreated DESC'; + $sortoptions = 'timemodified DESC'; } } @@ -243,15 +236,6 @@ function sortannotation($a, $b) { $this->page = $page; } - // Handling groups. - $currentgroups = groups_get_activity_group($this->cm, true); // Get a list of the currently allowed groups for this course. - - if ($currentgroups) { - $allowedusers = get_users_by_capability($this->context, 'mod/margic:addentries', '', $sort = 'lastname ASC, firstname ASC', '', '', $currentgroups); - } else { - $allowedusers = true; - } - // Get entries. if ($this->mode == 'allentries') { @@ -264,88 +248,6 @@ function sortannotation($a, $b) { } else if ($this->mode == 'ownentries') { $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id, 'baseentry' => null), $sortoptions); } - - $gradingstr = get_string('needsgrading', 'margic'); - $regradingstr = get_string('needsregrading', 'margic'); - - $viewinguserid = $USER->id; - - $strmanager = get_string_manager(); - - // Prepare entries. - // foreach ($this->entries as $i => $entry) { - // $this->entries[$i]->user = $DB->get_record('user', array('id' => $entry->userid)); - - // if (!$currentgroups || ($allowedusers && in_array($this->entries[$i]->user, $allowedusers))) { - // // Get child entries for entry. - // $this->entries[$i]->childentries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); - - // $revisionnr = count($this->entries[$i]->childentries); - // foreach ($this->entries[$i]->childentries as $ci => $childentry) { - // $this->entries[$i]->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager); - // $this->entries[$i]->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); - // $this->entries[$i]->childentries[$ci]->revision = $revisionnr; - - // if ($ci == array_key_first($this->entries[$i]->childentries)) { - // $this->entries[$i]->childentries[$ci]->newestentry = true; - // if ($viewinguserid == $childentry->userid) { - // $this->entries[$i]->childentries[$ci]->entrycanbeedited = true; - // } else { - // $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; - // } - // } else { - // $this->entries[$i]->childentries[$ci]->entrycanbeedited = false; - // $this->entries[$i]->childentries[$ci]->newestentry = false; - // } - - // if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { - // $this->entries[$i]->entrycanbeedited = true; - // } else { - // $this->entries[$i]->entrycanbeedited = false; - // } - - // $revisionnr -= 1; - // } - - // $this->entries[$i]->childentries = array_values($this->entries[$i]->childentries); - - // if (empty($this->entries[$i]->childentries)) { - // $this->entries[$i]->haschildren = false; - // } else { - // $this->entries[$i]->haschildren = true; - // } - - // // Get entry stats. - // $this->entries[$i]->stats = entrystats::get_entry_stats($entry->text, $entry->timecreated); - - // // Check entry grading. - // if (!empty($entry->timecreated) && empty($entry->timemarked)) { - // $this->entries[$i]->needsgrading = $gradingstr; - // } else if (!empty($entry->timemodified) && !empty($entry->timemarked) && $entry->timemodified > $entry->timemarked) { - // $this->entries[$i]->needsregrading = $regradingstr; - // } else { - // $this->entries[$i]->needsregrading = false; - // } - - // // Check if entry can be edited. - // if ($viewinguserid == $entry->userid && empty($this->entries[$i]->childentries)) { - // $this->entries[$i]->entrycanbeedited = true; - // } else { - // $this->entries[$i]->entrycanbeedited = false; - // } - - // // Prepare entry annotations. - // $this->entries[$i] = $this->prepare_entry_annotations($entry, $strmanager); - // } else { - // unset($this->entries[$i]); - // } - - // Replace base entry with last child entry for displaying last child entry on top - // if (!empty($this->entries[$i]->childentries)) { - // $baseentry = $this->entries[$i]; - // $this->entries[$i] = $this->entries[$i]->childentries[array_key_last($this->entries[$i]->childentries)]; - // } - //} } /** diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index af2fb68..9b822f7 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -24,18 +24,21 @@ {{#js}} {{/js}} -
+
{{revision}}. {{#str}}revision, mod_margic{{/str}} {{#user}}{{#userpicture}} - {{#str}}from, mod_margic {{/str}} - {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} + {{#canmanageentries}} + {{#str}}from, mod_margic {{/str}} + {{{userpicture}}} + {{/canmanageentries}} {{/userpicture}}{{/user}} {{#str}}at, mod_margic {{/str}} {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} + {{#newestentry}}{{/newestentry}}
{{#text}} @@ -49,10 +52,9 @@ {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}}
{{/stats}} - {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#timemodified}}{{#str}}lastedited, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} - - {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}} + {{/timemodified}}
{{#annotationmode}} diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index 121720a..ea40400 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -94,88 +94,92 @@ {{#childentries}} {{> margic/margic_childentry }} {{/childentries}} -
-
-
- {{^haschildren}} - {{#str}}entry, mod_margic{{/str}} - {{/haschildren}} - {{#haschildren}} - {{#str}}baseentry, mod_margic{{/str}} - {{/haschildren}} +
+
+
+
+ {{^haschildren}} + {{#str}}entry, mod_margic{{/str}} + {{/haschildren}} + {{#haschildren}} + {{#str}}baseentry, mod_margic{{/str}} + {{/haschildren}} - {{#user}}{{#userpicture}} - {{#str}}from, mod_margic {{/str}} - {{#canmanageentries}}{{{userpicture}}}{{/canmanageentries}} - {{/userpicture}}{{/user}} - {{#str}}at, mod_margic {{/str}} - {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} - {{^readonly}} - {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} - {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} - {{/readonly}} -
+ {{#user}}{{#userpicture}} + {{#canmanageentries}} + {{#str}}from, mod_margic {{/str}} + {{{userpicture}}} + {{/canmanageentries}} + {{/userpicture}}{{/user}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{^readonly}} + {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} + {{/readonly}} +
- {{#text}} -
- {{{text}}} + {{#text}} +
+ {{{text}}} +
+ {{/text}} + {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#stats}} + {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} +
+ {{/stats}} + {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} + + {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}}
- {{/text}} - {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} -
- {{#stats}} - {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} -
- {{/stats}} - {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} - - {{#timemodified}}{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}
{{/timemodified}}
-
- {{#annotationmode}} -
- {{#annotations}} -
-
- - {{type}} - -
-
-
- - {{{userpicturestr}}} - - - {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - + {{#annotationmode}} +
+ {{#annotations}} +
+
+ + {{type}} +
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}}
- - {{#text}}{{text}}{{/text}} - {{^text}}-{{/text}} - - {{#canbeedited}} - - {{/canbeedited}}
-
- {{/annotations}} + {{/annotations}} - {{#annotationform}} -
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ {{{annotationform}}}
- {{{annotationform}}} -
- {{/annotationform}} -
- {{/annotationmode}} + {{/annotationform}} +
+ {{/annotationmode}} +
diff --git a/view.php b/view.php index 1e5d908..5f3cb10 100644 --- a/view.php +++ b/view.php @@ -158,41 +158,19 @@ $currentuserrating = false; } -// Calculate if edit time has started. -$timenow = time(); -if (!$moduleinstance->timeopen) { - $edittimenotstarted = false; - $edittimestarts = false; -} else if ($moduleinstance->timeopen && $timenow >= $moduleinstance->timeopen) { - $edittimenotstarted = false; - $edittimestarts = $moduleinstance->timeopen; -} else if ($moduleinstance->timeopen && $timenow < $moduleinstance->timeopen) { - $edittimenotstarted = true; - $edittimestarts = $moduleinstance->timeopen; -} - -// Calculate if edit time has ended. -if (!$moduleinstance->timeclose) { - $edittimehasended = false; - $edittimeends = false; -} else if ($moduleinstance->timeclose && $timenow < $moduleinstance->timeclose) { - $edittimehasended = false; - $edittimeends = $moduleinstance->timeclose; -} else if ($moduleinstance->timeclose && $timenow >= $moduleinstance->timeclose) { - $edittimehasended = true; - $edittimeends = $moduleinstance->timeclose; -} - - // Handle groups. echo groups_print_activity_menu($cm, $CFG->wwwroot . "/mod/margic/view.php?id=$id"); +$edittimes = results::margic_get_edittime_options($moduleinstance); + // Output page. -$page = new margic_view($margic, $cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), $margic->get_sortmode(), - get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), $margic->get_annotationarea_width(), - $moduleinstance->editall, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, sesskey(), $currentuserrating, - $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), $margic->get_pagebar(), count($margic->get_entries()), - $annotationmode, $canmakeannotations, $margic->get_errortypes_for_form()); +$page = new margic_view($margic, $cm, $context, $moduleinstance, $margic->get_entries_grouped_by_pagecount(), + $margic->get_sortmode(), get_config('mod_margic', 'entrybgc'), get_config('mod_margic', 'entrytextbgc'), + $margic->get_annotationarea_width(), $moduleinstance->editall, $edittimes->edittimestarts, + $edittimes->edittimenotstarted, $edittimes->edittimeends, $edittimes->edittimehasended, $canmanageentries, + sesskey(), $currentuserrating, $ratingaggregationmode, $course, $userid, $margic->get_pagecountoptions(), + $margic->get_pagebar(), count($margic->get_entries()), $annotationmode, $canmakeannotations, + $margic->get_errortypes_for_form()); echo $OUTPUT->render($page); From 2ced09e1e514bc4ede4c2b9a1046df4a1771fd88 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 1 Aug 2022 13:42:59 +0200 Subject: [PATCH 23/60] feat (edit): rework of editing entries now finished --- backup/moodle2/backup_margic_stepslib.php | 2 +- backup/moodle2/restore_margic_stepslib.php | 8 +++-- classes/privacy/provider.php | 38 ++++++++++++++++++---- edit.php | 17 ++++++++++ lang/de/margic.php | 2 ++ lang/en/margic.php | 2 ++ 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php index 0a56c4b..e4a1844 100644 --- a/backup/moodle2/backup_margic_stepslib.php +++ b/backup/moodle2/backup_margic_stepslib.php @@ -50,7 +50,7 @@ protected function define_structure() { $entry = new backup_nested_element('entry', array('id'), array( 'userid', 'timecreated', 'timemodified', 'text', 'format', 'rating', 'entrycomment', 'formatcomment', 'teacher', - 'timemarked', 'mailed')); + 'timemarked', 'mailed', 'baseentry')); $annotations = new backup_nested_element('annotations'); $annotation = new backup_nested_element('annotation', array('id'), array( diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php index e3ee1c9..8abcf31 100644 --- a/backup/moodle2/restore_margic_stepslib.php +++ b/backup/moodle2/restore_margic_stepslib.php @@ -61,8 +61,6 @@ protected function define_structure() { protected function process_margic($data) { global $DB; - $userinfo = $this->get_setting_value('userinfo'); - $data = (object) $data; $oldid = $data->id; $data->course = $this->get_courseid(); @@ -114,6 +112,12 @@ protected function process_margic_entry($data) { $data->margic = $this->get_new_parentid('margic'); $data->userid = $this->get_mappingid('user', $data->userid); + if ($data->baseentry !== null && $this->get_mappingid('margic_entry', $data->baseentry) !== null) { + $data->baseentry = $this->get_mappingid('margic_entry', $data->baseentry); + } else { + $data->baseentry = null; + } + $newitemid = $DB->insert_record('margic_entries', $data); $this->set_mapping('margic_entry', $oldid, $newitemid); } diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 6a30207..d6f83b5 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -67,6 +67,7 @@ public static function get_metadata(collection $items): collection { 'entrycomment' => 'privacy:metadata:margic_entries:entrycomment', 'teacher' => 'privacy:metadata:margic_entries:teacher', 'timemarked' => 'privacy:metadata:margic_entries:timemarked', + 'baseentry' => 'privacy:metadata:margic_entries:baseentry', ], 'privacy:metadata:margic_entries'); // The table 'margic_annotations' stores the annotations made in all margics. @@ -267,7 +268,8 @@ protected static function export_entries_data(int $userid, $margicid, $margiccon e.entrycomment, e.formatcomment, e.teacher, - e.timemarked + e.timemarked, + e.baseentry FROM {margic_entries} e WHERE ( e.margic = :margicid AND @@ -304,15 +306,34 @@ protected static function export_entries_data(int $userid, $margicid, $margiccon */ protected static function export_entry_data(int $userid, \context $context, $subcontext, $entry) { + if ($entry->timecreated != 0) { + $timecreated = transform::datetime($entry->timecreated); + } else { + $timecreated = null; + } + + if ($entry->timemodified != 0) { + $timemodified = transform::datetime($entry->timemodified); + } else { + $timemodified = null; + } + + if ($entry->timemarked != 0) { + $timemarked = transform::datetime($entry->timemarked); + } else { + $timemarked = null; + } + // Store related metadata. $entrydata = (object) [ 'margic' => $entry->margic, 'userid' => $entry->userid, - 'timecreated' => transform::datetime($entry->timecreated), - 'timemodified' => transform::datetime($entry->timemodified), + 'timecreated' => $timecreated, + 'timemodified' => $timemodified, 'rating' => $entry->rating, 'teacher' => $entry->teacher, - 'timemarked' => transform::datetime($entry->timemarked), + 'timemarked' => $timemarked, + 'baseentry' => $entry->baseentry, ]; $entrydata->text = writer::with_context($context)->rewrite_pluginfile_urls($subcontext, 'mod_margic', 'entry', $entry->id, $entry->text); @@ -395,13 +416,19 @@ protected static function export_annotations_data(int $userid, $margicid, $margi */ protected static function export_annotation_data(int $userid, \context $context, $subcontext, $annotation) { + if ($annotation->timemodified != 0) { + $timemodified = transform::datetime($annotation->timemodified); + } else { + $timemodified = null; + } + // Store related metadata. $annotationdata = (object) [ 'margic' => $annotation->margic, 'entry' => $annotation->entry, 'userid' => $annotation->userid, 'timecreated' => transform::datetime($annotation->timecreated), - 'timemodified' => transform::datetime($annotation->timemodified), + 'timemodified' => $timemodified, 'type' => $annotation->type, 'text' => format_text($annotation->text, 2, array('para' => false)), ]; @@ -528,7 +555,6 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { ]); } - } } diff --git a/edit.php b/edit.php index df3922c..2e2076e 100644 --- a/edit.php +++ b/edit.php @@ -182,6 +182,23 @@ $newentry->baseentry = $entry->baseentry; } + // Check if timecreated is not older then connected entries + if ($moduleinstance->editdates) { + + $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry)); + + if ($newentry->timecreated < $baseentry->timemodified) { + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('timecreatedinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); + } + + $connectedentries = $DB->get_records('margic_entries', array('margic' => $moduleinstance->id, 'baseentry' => $newentry->baseentry), 'timecreated DESC'); + + if ($connectedentries && $newentry->timecreated < $connectedentries[array_key_first($connectedentries)]->timecreated) { + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('timecreatedinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); + } + + } + // Update timemodified for base entry. $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry)); $baseentry->timemodified = $fromform->timecreated; diff --git a/lang/de/margic.php b/lang/de/margic.php index 7b9229b..77456af 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -268,6 +268,7 @@ $string['at'] = 'am'; $string['from'] = 'von'; $string['toggleolderversions'] = 'Ältere Versionen ein- oder ausblenden'; +$string['timecreatedinvalid'] = 'Änderung fehlgeschlagen. Es gibt bereits jüngere Versionen dieses Beitrags.'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; @@ -282,6 +283,7 @@ $string['privacy:metadata:margic_entries:entrycomment'] = 'Der Kommentar des Lehrers zu diesem Eintrag.'; $string['privacy:metadata:margic_entries:teacher'] = 'ID der Bewerterin oder des Bewerters.'; $string['privacy:metadata:margic_entries:timemarked'] = 'Zeitpunkt der Bewertung.'; +$string['privacy:metadata:margic_entries:baseentry'] = 'Die ID des Originaleintrags auf dem dieser überarbeitete Eintrag basiert.'; $string['privacy:metadata:margic_annotations:margic'] = 'ID des Margics, zu dem der annotierte Eintrag gehört.'; $string['privacy:metadata:margic_annotations:entry'] = 'ID des Eintrags, zu dem die Annotation gehört.'; $string['privacy:metadata:margic_annotations:userid'] = 'ID des Benutzers, der die Annotation angelegt hat.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index a1865d9..6036968 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -281,6 +281,7 @@ $string['at'] = 'at'; $string['from'] = 'from'; $string['toggleolderversions'] = 'Toggle older versions of the entry'; +$string['timecreatedinvalid'] = 'Change failed. There are already younger versions of this entry.'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; @@ -295,6 +296,7 @@ $string['privacy:metadata:margic_entries:entrycomment'] = 'The teachers comment for the entry.'; $string['privacy:metadata:margic_entries:teacher'] = 'ID of the grader.'; $string['privacy:metadata:margic_entries:timemarked'] = 'Time the entry was graded.'; +$string['privacy:metadata:margic_entries:baseentry'] = 'The ID of the original entry on which this revised entry is based'; $string['privacy:metadata:margic_annotations:margic'] = 'ID of the Margic the annotated entry belongs to.'; $string['privacy:metadata:margic_annotations:entry'] = 'ID of the entry the annotation belongs to.'; $string['privacy:metadata:margic_annotations:userid'] = 'ID of the user that made the annotation.'; From 69299d836ea95617603828ca24c6de4e7680d80a Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 1 Aug 2022 19:24:17 +0200 Subject: [PATCH 24/60] feat (js): rewrite js as es6 module --- amd/build/annotations.min.js | 9 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 11 +- annotations.php | 1 - edit.php | 7 +- templates/margic_annotations_summary.mustache | 173 ++++++++-------- templates/margic_childentry.mustache | 141 +++++++------ templates/margic_entry.mustache | 12 +- templates/margic_view.mustache | 185 +++++++++--------- view.php | 3 +- 10 files changed, 264 insertions(+), 280 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 352b4f5..449c420 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,10 +1,3 @@ -function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from($("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n define(['jquery'], function($) {\n return {\n init: function(annotations, canmakeannotations) {\n\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n if (canmakeannotations) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n }\n };\n});"],"names":["define","$","init","annotations","canmakeannotations","editAnnotation","annotationid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n import $ from 'jquery';\n\n export const init = (annotations, canmakeannotations, myuserid) => {\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","editAnnotation","annotationid","userid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index d5b6313..fc828fb 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -21,10 +21,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - define(['jquery'], function($) { - return { - init: function(annotations, canmakeannotations) { + import $ from 'jquery'; + export const init = (annotations, canmakeannotations, myuserid) => { // Hide all Moodle forms. $('.annotation-form').hide(); @@ -67,7 +66,7 @@ * @param {int} annotationid */ function editAnnotation(annotationid) { - if (canmakeannotations) { + if (canmakeannotations && myuserid == annotations[annotationid].userid) { removeAllTempHighlights(); resetForms(); @@ -600,6 +599,4 @@ } }); - } - }; -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/annotations.php b/annotations.php index 9f1cd3b..3952d69 100644 --- a/annotations.php +++ b/annotations.php @@ -70,7 +70,6 @@ // Header. $PAGE->set_url('/mod/margic/annotations.php', array('id' => $id)); $PAGE->set_title(format_string($moduleinstance->name)); -$PAGE->set_heading($course->fullname); $urlparams = array('id' => $id, 'annotationmode' => 1); diff --git a/edit.php b/edit.php index 2e2076e..95c0cd2 100644 --- a/edit.php +++ b/edit.php @@ -251,10 +251,11 @@ echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($moduleinstance->name)); -$intro = format_module_intro('margic', $moduleinstance, $cm->id); -echo $OUTPUT->box($intro); +if ($moduleinstance->intro) { + echo $OUTPUT->box(format_module_intro('margic', $moduleinstance, $cm->id), 'generalbox mod_introbox', 'newmoduleintro'); +} -echo $OUTPUT->heading($title, 3); +echo $OUTPUT->heading($title, 4); // If existing entry is edited render entry. if ($entry) { diff --git a/templates/margic_annotations_summary.mustache b/templates/margic_annotations_summary.mustache index 71d839a..b1ffbb4 100644 --- a/templates/margic_annotations_summary.mustache +++ b/templates/margic_annotations_summary.mustache @@ -21,100 +21,99 @@ Annotations summary. }} -{{#js}} -{{/js}} +
+
+
+ + {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}modulename, mod_margic{{/str}}) +
-
-
- - {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}modulename, mod_margic{{/str}}) -
- -

{{#str}}margicerrortypes, mod_margic{{/str}}

- - - - - {{#margicerrortypes}} - - {{/margicerrortypes}} - - - {{#participants}} - - {{#errors}}{{/errors}} - {{/participants}} - -
{{#str}}participant, mod_margic{{/str}} - {{name}} -
- {{#defaulttype}}(S){{/defaulttype}} - {{^defaulttype}}(M){{/defaulttype}} - {{#canbeedited}} - - - {{/canbeedited}} - - -
-
{{firstname}} {{lastname}}{{.}}
-
- -
-
- - {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}template, mod_margic{{/str}}) -
- -

{{#str}}errortypetemplates, mod_margic{{/str}}

+

{{#str}}margicerrortypes, mod_margic{{/str}}

- {{#errortypetemplates.0}} - - - - - - + + {{#margicerrortypes}} + + {{/margicerrortypes}} - {{#errortypetemplates}} - - - - - - - - - {{/errortypetemplates}} + {{#participants}} + + {{#errors}}{{/errors}} + {{/participants}}
- {{#str}}name{{/str}} - - {{#str}}type, mod_margic{{/str}} - - {{#str}}color, mod_margic{{/str}} - - {{#str}}edit{{/str}} - - {{#str}}delete{{/str}} - - {{#str}}addtomargic, mod_margic{{/str}} - {{#str}}participant, mod_margic{{/str}} + {{name}} +
+ {{#defaulttype}}(S){{/defaulttype}} + {{^defaulttype}}(M){{/defaulttype}} + {{#canbeedited}} + + + {{/canbeedited}} + + +
+
{{name}} - {{type}} - - {{#canbeedited}}{{/canbeedited}} - - {{#canbeedited}}{{/canbeedited}} - - -
{{firstname}} {{lastname}}{{.}}
- {{/errortypetemplates.0}} - {{^errortypetemplates.0}} {{#str}}notemplatetypes, mod_margic{{/str}} {{/errortypetemplates.0}} -
+
+ +
+
+ + {{#str}}adderrortype, mod_margic{{/str}} ({{#str}}template, mod_margic{{/str}}) +
+ +

{{#str}}errortypetemplates, mod_margic{{/str}}

-
- {{#str}}backtooverview, mod_margic{{/str}} + {{#errortypetemplates.0}} + + + + + + + + + + + {{#errortypetemplates}} + + + + + + + + + {{/errortypetemplates}} + +
+ {{#str}}name{{/str}} + + {{#str}}type, mod_margic{{/str}} + + {{#str}}color, mod_margic{{/str}} + + {{#str}}edit{{/str}} + + {{#str}}delete{{/str}} + + {{#str}}addtomargic, mod_margic{{/str}} +
{{name}} + {{type}} + + {{#canbeedited}}{{/canbeedited}} + + {{#canbeedited}}{{/canbeedited}} + + +
+ {{/errortypetemplates.0}} + {{^errortypetemplates.0}} {{#str}}notemplatetypes, mod_margic{{/str}} {{/errortypetemplates.0}} +
+ +
\ No newline at end of file diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index 9b822f7..74efbce 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -21,84 +21,83 @@ Template for single child entry. }} -{{#js}} -{{/js}} +
+
+
+
+
{{revision}}. {{#str}}revision, mod_margic{{/str}} + {{#user}}{{#userpicture}} + {{#canmanageentries}} + {{#str}}from, mod_margic {{/str}} + {{{userpicture}}} + {{/canmanageentries}} + {{/userpicture}}{{/user}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} + {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} + {{#newestentry}}{{/newestentry}} +
-
-
-
-
{{revision}}. {{#str}}revision, mod_margic{{/str}} - {{#user}}{{#userpicture}} - {{#canmanageentries}} - {{#str}}from, mod_margic {{/str}} - {{{userpicture}}} - {{/canmanageentries}} - {{/userpicture}}{{/user}} - {{#str}}at, mod_margic {{/str}} - {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} - {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} - {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} - {{#newestentry}}{{/newestentry}} -
- - {{#text}} -
- {{{text}}} + {{#text}} +
+ {{{text}}} +
+ {{/text}} + {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} +
+ {{#stats}} + {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} +
+ {{/stats}} + {{#timemodified}}{{#str}}lastedited, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} + {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} + {{/timemodified}}
- {{/text}} - {{^text}}

{{#str}}blankentry, mod_margic{{/str}}

{{/text}} -
- {{#stats}} - {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}} -
- {{/stats}} - {{#timemodified}}{{#str}}lastedited, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}})
{{/datediff}}{{/stats}} - {{/timemodified}}
-
- {{#annotationmode}} -
- {{#annotations}} -
-
- - {{type}} - -
-
-
- - {{{userpicturestr}}} - - - {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - + {{#annotationmode}} +
+ {{#annotations}} +
+
+ + {{type}} +
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+
+ + {{{userpicturestr}}} + + + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ + {{#text}}{{text}}{{/text}} + {{^text}}-{{/text}} + + {{#canbeedited}} + + {{/canbeedited}}
- - {{#text}}{{text}}{{/text}} - {{^text}}-{{/text}} - - {{#canbeedited}} - - {{/canbeedited}}
-
- {{/annotations}} + {{/annotations}} - {{#annotationform}} -
-
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} + {{#annotationform}} +
+
+ {{#str}}annotatedtextnotfound, mod_margic {{/str}} +
+ {{{annotationform}}}
- {{{annotationform}}} -
- {{/annotationform}} -
- {{/annotationmode}} + {{/annotationform}} +
+ {{/annotationmode}} +
\ No newline at end of file diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index ea40400..fcd3ae5 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -85,12 +85,8 @@ } }} -{{#js}} -{{/js}} - -{{#entry}} -
- +
+ {{#entry}} {{#childentries}} {{> margic/margic_childentry }} {{/childentries}} @@ -208,5 +204,5 @@
{{needsregrading}}
{{/needsregrading}}
-
-{{/entry}} \ No newline at end of file + {{/entry}} +
\ No newline at end of file diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 7e29572..889a6d7 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -21,100 +21,99 @@ Overview. }} -{{#js}} -{{/js}} - -{{#edittimehasended}}{{#edittimeends}}{{/edittimeends}}{{/edittimehasended}} -{{#edittimenotstarted}}{{#edittimestarts}}{{/edittimestarts}}{{/edittimenotstarted}} - -

{{#str}}overview, mod_margic{{/str}}

- -
- {{#ratingaggregationmode}}{{#entries.0}} - {{ratingaggregationmode}}: - - {{#currentuserrating}} - {{currentuserrating}} - {{/currentuserrating}} - {{/entries.0}}{{/ratingaggregationmode}} - {{^ratingaggregationmode}}{{#str}}norating, mod_margic{{/str}}{{/ratingaggregationmode}} - {{#entries.0}} - {{#str}}sorting, mod_margic{{/str}}{{#sortmode}}: {{sortmode}}{{/sortmode}} - {{/entries.0}} -
- -
- {{^edittimehasended}}{{^edittimenotstarted}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimenotstarted}}{{/edittimehasended}} +
+ {{#edittimehasended}}{{#edittimeends}}{{/edittimeends}}{{/edittimehasended}} + {{#edittimenotstarted}}{{#edittimestarts}}{{/edittimestarts}}{{/edittimenotstarted}} + +

{{#str}}overview, mod_margic{{/str}}

+ +
+ {{#ratingaggregationmode}}{{#entries.0}} + {{ratingaggregationmode}}: + + {{#currentuserrating}} + {{currentuserrating}} + {{/currentuserrating}} + {{/entries.0}}{{/ratingaggregationmode}} + {{^ratingaggregationmode}}{{#str}}norating, mod_margic{{/str}}{{/ratingaggregationmode}} + {{#entries.0}} + {{#str}}sorting, mod_margic{{/str}}{{#sortmode}}: {{sortmode}}{{/sortmode}} + {{/entries.0}} +
+ +
+ {{^edittimehasended}}{{^edittimenotstarted}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimenotstarted}}{{/edittimehasended}} + {{#entries.0}} + {{#canmanageentries}}{{/canmanageentries}} + {{#canmanageentries}}{{#singleuser}} {{#str}}viewallentries, mod_margic{{/str}} {{/singleuser}}{{/canmanageentries}} + {{^annotationmode}} + {{#canmakeannotations}} {{#str}}viewandmakeannotations, mod_margic{{/str}} {{/canmakeannotations}} + {{^canmakeannotations}} {{#str}}viewannotations, mod_margic{{/str}} {{/canmakeannotations}} + {{/annotationmode}} + {{#annotationmode}} + {{#str}}hideannotations, mod_margic{{/str}} + + {{/annotationmode}} + {{/entries.0}} + {{#canmanageentries}} {{#str}}annotationssummary, mod_margic{{/str}} {{/canmanageentries}} + + {{#entries.0}} + + + + {{#ratingaggregationmode}} + + + {{/ratingaggregationmode}} + {{/entries.0}} +
+ {{#entries.0}} - {{#canmanageentries}}{{/canmanageentries}} - {{#canmanageentries}}{{#singleuser}} {{#str}}viewallentries, mod_margic{{/str}} {{/singleuser}}{{/canmanageentries}} - {{^annotationmode}} - {{#canmakeannotations}} {{#str}}viewandmakeannotations, mod_margic{{/str}} {{/canmakeannotations}} - {{^canmakeannotations}} {{#str}}viewannotations, mod_margic{{/str}} {{/canmakeannotations}} - {{/annotationmode}} +
+ + + +
+
    + {{#pagebar}} +
  • + {{{display}}} +
  • + {{/pagebar}} + +
    + {{#str}}pagesize, mod_margic{{/str}}: + + / {{entriescount}} +
    +
+
+
+ +
+
+

+ {{#canmanageentries}}{{#str}}entries, mod_margic{{/str}}{{/canmanageentries}} + {{^canmanageentries}}{{#str}}myentries, mod_margic{{/str}}{{/canmanageentries}} +

+
{{#annotationmode}} - {{#str}}hideannotations, mod_margic{{/str}} - - {{/annotationmode}} - {{/entries.0}} - {{#canmanageentries}} {{#str}}annotationssummary, mod_margic{{/str}} {{/canmanageentries}} - - {{#entries.0}} - - - - {{#ratingaggregationmode}} - - - {{/ratingaggregationmode}} - {{/entries.0}} -
- -{{#entries.0}} -
- - - -
-
    - {{#pagebar}} -
  • - {{{display}}} -
  • - {{/pagebar}} - -
    - {{#str}}pagesize, mod_margic{{/str}}: - - / {{entriescount}} +
    +

    {{#str}} annotations, mod_margic {{/str}}

    -
-
-
- -
-
-

- {{#canmanageentries}}{{#str}}entries, mod_margic{{/str}}{{/canmanageentries}} - {{^canmanageentries}}{{#str}}myentries, mod_margic{{/str}}{{/canmanageentries}} -

+ {{/annotationmode}}
- {{#annotationmode}} -
-

{{#str}} annotations, mod_margic {{/str}}

-
- {{/annotationmode}} -
-{{/entries.0}} - -{{#entries}} - {{{entry}}} -{{/entries}} - -{{^entries}} -
- {{#canmanageentries}}{{#str}}noentriesfound, mod_margic{{/str}}{{/canmanageentries}} - {{^canmanageentries}}{{#str}}notstarted, mod_margic{{/str}}{{/canmanageentries}} -{{/entries}} \ No newline at end of file + {{/entries.0}} + + {{#entries}} + {{{entry}}} + {{/entries}} + + {{^entries}} +
+ {{#canmanageentries}}{{#str}}noentriesfound, mod_margic{{/str}}{{/canmanageentries}} + {{^canmanageentries}}{{#str}}notstarted, mod_margic{{/str}}{{/canmanageentries}} + {{/entries}} +
\ No newline at end of file diff --git a/view.php b/view.php index 5f3cb10..37b8605 100644 --- a/view.php +++ b/view.php @@ -126,7 +126,8 @@ $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', array('annotations' => $margic->get_annotations(), - 'canmakeannotations' => $canmakeannotations)); + 'canmakeannotations' => $canmakeannotations, + 'myuserid' => $USER->id)); } else { // Header. $PAGE->set_url('/mod/margic/view.php', array( From bf61a32519ca8a64d75e1447a2d451ea9b368688 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 2 Aug 2022 11:50:52 +0200 Subject: [PATCH 25/60] feat (js): minor improvements --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 1094 +++++++++++++++--------------- 3 files changed, 554 insertions(+), 544 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 449c420..4b8a97f 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,3 +1,3 @@ -define("mod_margic/annotations",["exports","jquery"],(function(_exports,_jquery){var obj;function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n import $ from 'jquery';\n\n export const init = (annotations, canmakeannotations, myuserid) => {\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights();\n resetForms();\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function() {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function() {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function(e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","editAnnotation","annotationid","userid","removeAllTempHighlights","resetForms","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (annotations, canmakeannotations, myuserid) => {\n var edited = false;\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function (e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","edited","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index fc828fb..923d817 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -21,582 +21,592 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - import $ from 'jquery'; - - export const init = (annotations, canmakeannotations, myuserid) => { - // Hide all Moodle forms. - $('.annotation-form').hide(); - - // Remove col-mds from moodle form. - $('.annotation-form div.col-md-3').removeClass('col-md-3'); - $('.annotation-form div.col-md-9').removeClass('col-md-9'); - $('.annotation-form div.form-group').removeClass('form-group'); - $('.annotation-form div.row').removeClass('row'); - - /** - * Recreate annotations. - * - */ - function recreateAnnotations() { - for (let annotation of Object.values(annotations)) { - - // Recreate range from db. - var newrange = document.createRange(); - - try { - newrange.setStart( - nodeFromXPath(annotation.startcontainer, $("#entry-" + annotation.entry)[0]), annotation.startposition); - newrange.setEnd( - nodeFromXPath(annotation.endcontainer, $("#entry-" + annotation.entry)[0]), annotation.endposition); - } catch (e) { - // eslint-disable-line - } - - var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color); - - if (annotatedtext != '') { - $('#annotationpreview-' + annotation.id).html(annotatedtext); - } - } +import $ from 'jquery'; + +export const init = (annotations, canmakeannotations, myuserid) => { + var edited = false; + // Hide all Moodle forms. + $('.annotation-form').hide(); + + // Remove col-mds from moodle form. + $('.annotation-form div.col-md-3').removeClass('col-md-3'); + $('.annotation-form div.col-md-9').removeClass('col-md-9'); + $('.annotation-form div.form-group').removeClass('form-group'); + $('.annotation-form div.row').removeClass('row'); + + /** + * Recreate annotations. + * + */ + function recreateAnnotations() { + for (let annotation of Object.values(annotations)) { + + // Recreate range from db. + var newrange = document.createRange(); + + try { + newrange.setStart( + nodeFromXPath(annotation.startcontainer, $("#entry-" + annotation.entry)[0]), annotation.startposition); + newrange.setEnd( + nodeFromXPath(annotation.endcontainer, $("#entry-" + annotation.entry)[0]), annotation.endposition); + } catch (e) { + // eslint-disable-line } - /** - * Edit annotation. - * - * @param {int} annotationid - */ - function editAnnotation(annotationid) { - if (canmakeannotations && myuserid == annotations[annotationid].userid) { - removeAllTempHighlights(); - resetForms(); - - var entry = annotations[annotationid].entry; - - $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box. - - $('.annotation-form-' + entry + ' input[name="startcontainer"]').val(annotations[annotationid].startcontainer); - $('.annotation-form-' + entry + ' input[name="endcontainer"]').val(annotations[annotationid].endcontainer); - $('.annotation-form-' + entry + ' input[name="startposition"]').val(annotations[annotationid].startposition); - $('.annotation-form-' + entry + ' input[name="endposition"]').val(annotations[annotationid].endposition); - - $('.annotation-form-' + entry + ' input[name="annotationid"]').val(annotationid); - - $('.annotation-form-' + entry + ' textarea[name="text"]').val(annotations[annotationid].text); - - $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type); + var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color); - $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html()); - $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color); - - $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid); - $('.annotationarea-' + entry + ' .annotation-form').show(); - $('.annotationarea-' + entry + ' #id_text').focus(); - } else { - $('.annotation-box-' + annotationid).focus(); - } + if (annotatedtext != '') { + $('#annotationpreview-' + annotation.id).html(annotatedtext); } - - /** - * Reset all annotation forms - */ - function resetForms() { - $('.annotation-form').hide(); - - $('.annotation-form input[name^="annotationid"]').val(null); - - $('.annotation-form input[name^="startcontainer"]').val(-1); - $('.annotation-form input[name^="endcontainer"]').val(-1); - $('.annotation-form input[name^="startposition"]').val(-1); - $('.annotation-form input[name^="endposition"]').val(-1); - - $('.annotation-form textarea[name^="text"]').val(''); - - $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation. + } + } + + /** + * Edit annotation. + * + * @param {int} annotationid + */ + function editAnnotation(annotationid) { + + if (edited == annotationid) { + removeAllTempHighlights(); // Remove other temporary highlights. + resetForms(); // Remove old form contents. + edited = false; + } else if (canmakeannotations && myuserid == annotations[annotationid].userid) { + removeAllTempHighlights(); // Remove other temporary highlights. + resetForms(); // Remove old form contents. + + edited = annotationid; + + var entry = annotations[annotationid].entry; + + $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box. + + $('.annotation-form-' + entry + ' input[name="startcontainer"]').val(annotations[annotationid].startcontainer); + $('.annotation-form-' + entry + ' input[name="endcontainer"]').val(annotations[annotationid].endcontainer); + $('.annotation-form-' + entry + ' input[name="startposition"]').val(annotations[annotationid].startposition); + $('.annotation-form-' + entry + ' input[name="endposition"]').val(annotations[annotationid].endposition); + + $('.annotation-form-' + entry + ' input[name="annotationid"]').val(annotationid); + + $('.annotation-form-' + entry + ' textarea[name="text"]').val(annotations[annotationid].text); + + $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type); + + $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html()); + $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color); + + $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid); + $('.annotationarea-' + entry + ' .annotation-form').show(); + $('.annotationarea-' + entry + ' #id_text').focus(); + } else { + $('.annotation-box-' + annotationid).focus(); + } + } + + /** + * Reset all annotation forms + */ + function resetForms() { + $('.annotation-form').hide(); + + $('.annotation-form input[name^="annotationid"]').val(null); + + $('.annotation-form input[name^="startcontainer"]').val(-1); + $('.annotation-form input[name^="endcontainer"]').val(-1); + $('.annotation-form input[name^="startposition"]').val(-1); + $('.annotation-form input[name^="endposition"]').val(-1); + + $('.annotation-form textarea[name^="text"]').val(''); + + $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation. + } + + /** + * Return text nodes which are entirely inside `range`. + * + * If a range starts or ends part-way through a text node, the node is split + * and the part inside the range is returned. + * + * @param {Range} range + * @return {Text[]} + */ + function wholeTextNodesInRange(range) { + if (range.collapsed) { + // Exit early for an empty range to avoid an edge case that breaks the algorithm + // below. Splitting a text node at the start of an empty range can leave the + // range ending in the left part rather than the right part. + return []; + } + + /** @type {Node|null} */ + let root = range.commonAncestorContainer; + if (root.nodeType !== Node.ELEMENT_NODE) { + // If the common ancestor is not an element, set it to the parent element to + // ensure that the loop below visits any text nodes generated by splitting + // the common ancestor. + // + // Note that `parentElement` may be `null`. + root = root.parentElement; + } + if (!root) { + // If there is no root element then we won't be able to insert highlights, + // so exit here. + return []; + } + + const textNodes = []; + const nodeIter = /** @type {Document} */ ( + root.ownerDocument + ).createNodeIterator( + root, + NodeFilter.SHOW_TEXT // Only return `Text` nodes. + ); + let node; + while ((node = nodeIter.nextNode())) { + if (!isNodeInRange(range, node)) { + continue; } + let text = /** @type {Text} */ (node); - /** - * Return text nodes which are entirely inside `range`. - * - * If a range starts or ends part-way through a text node, the node is split - * and the part inside the range is returned. - * - * @param {Range} range - * @return {Text[]} - */ - function wholeTextNodesInRange(range) { - if (range.collapsed) { - // Exit early for an empty range to avoid an edge case that breaks the algorithm - // below. Splitting a text node at the start of an empty range can leave the - // range ending in the left part rather than the right part. - return []; - } - - /** @type {Node|null} */ - let root = range.commonAncestorContainer; - if (root.nodeType !== Node.ELEMENT_NODE) { - // If the common ancestor is not an element, set it to the parent element to - // ensure that the loop below visits any text nodes generated by splitting - // the common ancestor. - // - // Note that `parentElement` may be `null`. - root = root.parentElement; - } - if (!root) { - // If there is no root element then we won't be able to insert highlights, - // so exit here. - return []; - } - - const textNodes = []; - const nodeIter = /** @type {Document} */ ( - root.ownerDocument - ).createNodeIterator( - root, - NodeFilter.SHOW_TEXT // Only return `Text` nodes. - ); - let node; - while ((node = nodeIter.nextNode())) { - if (!isNodeInRange(range, node)) { - continue; - } - let text = /** @type {Text} */ (node); - - if (text === range.startContainer && range.startOffset > 0) { - // Split `text` where the range starts. The split will create a new `Text` - // node which will be in the range and will be visited in the next loop iteration. - text.splitText(range.startOffset); - continue; - } - - if (text === range.endContainer && range.endOffset < text.data.length) { - // Split `text` where the range ends, leaving it as the part in the range. - text.splitText(range.endOffset); - } - - textNodes.push(text); - } - - return textNodes; + if (text === range.startContainer && range.startOffset > 0) { + // Split `text` where the range starts. The split will create a new `Text` + // node which will be in the range and will be visited in the next loop iteration. + text.splitText(range.startOffset); + continue; } - /** - * Wraps the DOM Nodes within the provided range with a highlight - * element of the specified class and returns the highlight Elements. - * - * @param {Range} range - Range to be highlighted - * @param {int} annotationid - ID of annotation - * @param {string} cssClass - A CSS class to use for the highlight - * @param {string} color - Color of the highlighting - * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect - */ - function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') { - - const textNodes = wholeTextNodesInRange(range); - - // Group text nodes into spans of adjacent nodes. If a group of text nodes are - // adjacent, we only need to create one highlight element for the group. - let textNodeSpans = []; - let prevNode = null; - let currentSpan = null; - - textNodes.forEach(node => { - if (prevNode && prevNode.nextSibling === node) { - currentSpan.push(node); - } else { - currentSpan = [node]; - textNodeSpans.push(currentSpan); - } - prevNode = node; - }); - - // Filter out text node spans that consist only of white space. This avoids - // inserting highlight elements in places that can only contain a restricted - // subset of nodes such as table rows and lists. - const whitespace = /^\s*$/; - textNodeSpans = textNodeSpans.filter(span => - // Check for at least one text node with non-space content. - span.some(node => !whitespace.test(node.nodeValue)) - ); - - // Wrap each text node span with a `` element. - var hihglightedtext = ''; - - textNodeSpans.forEach(nodes => { - const highlightEl = document.createElement('span'); - highlightEl.className = cssClass; - - if (annotationid) { - highlightEl.className += ' ' + cssClass + '-' + annotationid; - // highlightEl.tabIndex = 1; - highlightEl.id = cssClass + '-' + annotationid; - highlightEl.style.backgroundColor = '#' + color; - } - - hihglightedtext += nodes[0].textContent; - - nodes[0].parentNode.replaceChild(highlightEl, nodes[0]); - nodes.forEach(node => highlightEl.appendChild(node)); - - }); - - return hihglightedtext; + if (text === range.endContainer && range.endOffset < text.data.length) { + // Split `text` where the range ends, leaving it as the part in the range. + text.splitText(range.endOffset); } - /** - * Returns true if any part of `node` lies within `range`. - * - * @param {Range} range - * @param {Node} node - * @return {bool} - If node is in range - */ - function isNodeInRange(range, node) { - try { - const length = node.nodeValue?.length ?? node.childNodes.length; - return ( - // Check start of node is before end of range. - range.comparePoint(node, 0) <= 0 && - // Check end of node is after start of range. - range.comparePoint(node, length) >= 0 - ); - } catch (e) { - // `comparePoint` may fail if the `range` and `node` do not share a common - // ancestor or `node` is a doctype. - return false; - } + textNodes.push(text); + } + + return textNodes; + } + + /** + * Wraps the DOM Nodes within the provided range with a highlight + * element of the specified class and returns the highlight Elements. + * + * @param {Range} range - Range to be highlighted + * @param {int} annotationid - ID of annotation + * @param {string} cssClass - A CSS class to use for the highlight + * @param {string} color - Color of the highlighting + * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect + */ + function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') { + + const textNodes = wholeTextNodesInRange(range); + + // Group text nodes into spans of adjacent nodes. If a group of text nodes are + // adjacent, we only need to create one highlight element for the group. + let textNodeSpans = []; + let prevNode = null; + let currentSpan = null; + + textNodes.forEach(node => { + if (prevNode && prevNode.nextSibling === node) { + currentSpan.push(node); + } else { + currentSpan = [node]; + textNodeSpans.push(currentSpan); } - - /** - * Get the node name for use in generating an xpath expression. - * - * @param {Node} node - * @return {string} - Name of the node - */ - function getNodeName(node) { - const nodeName = node.nodeName.toLowerCase(); - let result = nodeName; - if (nodeName === '#text') { - result = 'text()'; - } - return result; + prevNode = node; + }); + + // Filter out text node spans that consist only of white space. This avoids + // inserting highlight elements in places that can only contain a restricted + // subset of nodes such as table rows and lists. + const whitespace = /^\s*$/; + textNodeSpans = textNodeSpans.filter(span => + // Check for at least one text node with non-space content. + span.some(node => !whitespace.test(node.nodeValue)) + ); + + // Wrap each text node span with a `` element. + var hihglightedtext = ''; + + textNodeSpans.forEach(nodes => { + const highlightEl = document.createElement('span'); + highlightEl.className = cssClass; + + if (annotationid) { + highlightEl.className += ' ' + cssClass + '-' + annotationid; + // highlightEl.tabIndex = 1; + highlightEl.id = cssClass + '-' + annotationid; + highlightEl.style.backgroundColor = '#' + color; } - /** - * Get the index of the node as it appears in its parent's child list - * - * @param {Node} node - * @return {int} - Position of the node - */ - function getNodePosition(node) { - let pos = 0; - /** @type {Node|null} */ - let tmp = node; - while (tmp) { - if (tmp.nodeName === node.nodeName) { - pos += 1; - } - tmp = tmp.previousSibling; - } - return pos; + hihglightedtext += nodes[0].textContent; + + nodes[0].parentNode.replaceChild(highlightEl, nodes[0]); + nodes.forEach(node => highlightEl.appendChild(node)); + + }); + + return hihglightedtext; + } + + /** + * Returns true if any part of `node` lies within `range`. + * + * @param {Range} range + * @param {Node} node + * @return {bool} - If node is in range + */ + function isNodeInRange(range, node) { + try { + const length = node.nodeValue?.length ?? node.childNodes.length; + return ( + // Check start of node is before end of range. + range.comparePoint(node, 0) <= 0 && + // Check end of node is after start of range. + range.comparePoint(node, length) >= 0 + ); + } catch (e) { + // `comparePoint` may fail if the `range` and `node` do not share a common + // ancestor or `node` is a doctype. + return false; + } + } + + /** + * Get the node name for use in generating an xpath expression. + * + * @param {Node} node + * @return {string} - Name of the node + */ + function getNodeName(node) { + const nodeName = node.nodeName.toLowerCase(); + let result = nodeName; + if (nodeName === '#text') { + result = 'text()'; + } + return result; + } + + /** + * Get the index of the node as it appears in its parent's child list + * + * @param {Node} node + * @return {int} - Position of the node + */ + function getNodePosition(node) { + let pos = 0; + /** @type {Node|null} */ + let tmp = node; + while (tmp) { + if (tmp.nodeName === node.nodeName) { + pos += 1; } - - /** - * Get the path segments to the node - * - * @param {Node} node - * @return {array} - Path segments - */ - function getPathSegment(node) { - const name = getNodeName(node); - const pos = getNodePosition(node); - return `${name}[${pos}]`; - } - - /** - * A simple XPath generator which can generate XPaths of the form - * /tag[index]/tag[index]. - * - * @param {Node} node - The node to generate a path to - * @param {Node} root - Root node to which the returned path is relative - * @return {string} - The xpath of a node - */ - function xpathFromNode(node, root) { - let xpath = ''; - - /** @type {Node|null} */ - let elem = node; - while (elem !== root) { - if (!elem) { - throw new Error('Node is not a descendant of root'); - } - xpath = getPathSegment(elem) + '/' + xpath; - elem = elem.parentNode; - } - xpath = '/' + xpath; - xpath = xpath.replace(/\/$/, ''); // Remove trailing slash - - return xpath; + tmp = tmp.previousSibling; + } + return pos; + } + + /** + * Get the path segments to the node + * + * @param {Node} node + * @return {array} - Path segments + */ + function getPathSegment(node) { + const name = getNodeName(node); + const pos = getNodePosition(node); + return `${name}[${pos}]`; + } + + /** + * A simple XPath generator which can generate XPaths of the form + * /tag[index]/tag[index]. + * + * @param {Node} node - The node to generate a path to + * @param {Node} root - Root node to which the returned path is relative + * @return {string} - The xpath of a node + */ + function xpathFromNode(node, root) { + let xpath = ''; + + /** @type {Node|null} */ + let elem = node; + while (elem !== root) { + if (!elem) { + throw new Error('Node is not a descendant of root'); } - - /** - * Return the `index`'th immediate child of `element` whose tag name is - * `nodeName` (case insensitive). - * - * @param {Element} element - * @param {string} nodeName - * @param {number} index - * @return {Element|null} - The child element or null - */ - function nthChildOfType(element, nodeName, index) { - nodeName = nodeName.toUpperCase(); - - let matchIndex = -1; - for (let i = 0; i < element.children.length; i++) { - const child = element.children[i]; - if (child.nodeName.toUpperCase() === nodeName) { - ++matchIndex; - if (matchIndex === index) { + xpath = getPathSegment(elem) + '/' + xpath; + elem = elem.parentNode; + } + xpath = '/' + xpath; + xpath = xpath.replace(/\/$/, ''); // Remove trailing slash + + return xpath; + } + + /** + * Return the `index`'th immediate child of `element` whose tag name is + * `nodeName` (case insensitive). + * + * @param {Element} element + * @param {string} nodeName + * @param {number} index + * @return {Element|null} - The child element or null + */ + function nthChildOfType(element, nodeName, index) { + nodeName = nodeName.toUpperCase(); + + let matchIndex = -1; + for (let i = 0; i < element.children.length; i++) { + const child = element.children[i]; + if (child.nodeName.toUpperCase() === nodeName) { + ++matchIndex; + if (matchIndex === index) { return child; - } - } - } - - return null; - } - - /** - * Evaluate a _simple XPath_ relative to a `root` element and return the - * matching element. - * - * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings. - * - * Unlike `document.evaluate` this function: - * - * - Only supports simple XPaths - * - Is not affected by the document's _type_ (HTML or XML/XHTML) - * - Ignores element namespaces when matching element names in the XPath against - * elements in the DOM tree - * - Is case insensitive for all elements, not just HTML elements - * - * The matching element is returned or `null` if no such element is found. - * An error is thrown if `xpath` is not a simple XPath. - * - * @param {string} xpath - * @param {Element} root - * @return {Element|null} - */ - function evaluateSimpleXPath(xpath, root) { - const isSimpleXPath = xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/) !== null; - if (!isSimpleXPath) { - throw new Error('Expression is not a simple XPath'); } - - const segments = xpath.split('/'); - let element = root; - - // Remove leading empty segment. The regex above validates that the XPath - // has at least two segments, with the first being empty and the others non-empty. - segments.shift(); - - for (let segment of segments) { - let elementName; - let elementIndex; - - const separatorPos = segment.indexOf('['); - if (separatorPos !== -1) { - elementName = segment.slice(0, separatorPos); - - const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']')); - elementIndex = parseInt(indexStr) - 1; - if (elementIndex < 0) { - return null; - } - } else { - elementName = segment; - elementIndex = 0; - } - - const child = nthChildOfType(element, elementName, elementIndex); - if (!child) { - return null; - } - - element = child; - } - - return element; } - - /** - * Finds an element node using an XPath relative to `root` - * - * Example: - * node = nodeFromXPath('/main/article[1]/p[3]', document.body) - * - * @param {string} xpath - * @param {Element} [root] - * @return {Node|null} - */ - function nodeFromXPath(xpath, root = document.body) { - try { - return evaluateSimpleXPath(xpath, root); - } catch (err) { - return document.evaluate( - '.' + xpath, - root, - - // The `namespaceResolver` and `result` arguments are optional in the spec - // but required in Edge Legacy. - null /* NamespaceResolver */, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null /* Result */ - ).singleNodeValue; + } + + return null; + } + + /** + * Evaluate a _simple XPath_ relative to a `root` element and return the + * matching element. + * + * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings. + * + * Unlike `document.evaluate` this function: + * + * - Only supports simple XPaths + * - Is not affected by the document's _type_ (HTML or XML/XHTML) + * - Ignores element namespaces when matching element names in the XPath against + * elements in the DOM tree + * - Is case insensitive for all elements, not just HTML elements + * + * The matching element is returned or `null` if no such element is found. + * An error is thrown if `xpath` is not a simple XPath. + * + * @param {string} xpath + * @param {Element} root + * @return {Element|null} + */ + function evaluateSimpleXPath(xpath, root) { + const isSimpleXPath = xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/) !== null; + if (!isSimpleXPath) { + throw new Error('Expression is not a simple XPath'); + } + + const segments = xpath.split('/'); + let element = root; + + // Remove leading empty segment. The regex above validates that the XPath + // has at least two segments, with the first being empty and the others non-empty. + segments.shift(); + + for (let segment of segments) { + let elementName; + let elementIndex; + + const separatorPos = segment.indexOf('['); + if (separatorPos !== -1) { + elementName = segment.slice(0, separatorPos); + + const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']')); + elementIndex = parseInt(indexStr) - 1; + if (elementIndex < 0) { + return null; } + } else { + elementName = segment; + elementIndex = 0; } - /** - * Replace a child `node` with `replacements`. - * - * nb. This is like `ChildNode.replaceWith` but it works in older browsers. - * - * @param {ChildNode} node - * @param {Node[]} replacements - */ - function replaceWith(node, replacements) { - const parent = /** @type {Node} */ (node.parentNode); - - replacements.forEach(r => parent.insertBefore(r, node)); - node.remove(); + const child = nthChildOfType(element, elementName, elementIndex); + if (!child) { + return null; } - /** - * Remove all temporary highlights under a given root element. - */ - function removeAllTempHighlights() { - const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); - if (highlights !== undefined && highlights.length != 0) { - removeHighlights(highlights); - } - } + element = child; + } + + return element; + } + + /** + * Finds an element node using an XPath relative to `root` + * + * Example: + * node = nodeFromXPath('/main/article[1]/p[3]', document.body) + * + * @param {string} xpath + * @param {Element} [root] + * @return {Node|null} + */ + function nodeFromXPath(xpath, root = document.body) { + try { + return evaluateSimpleXPath(xpath, root); + } catch (err) { + return document.evaluate( + '.' + xpath, + root, - /** - * Remove highlights from a range previously highlighted with `highlightRange`. - * - * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange` - */ - function removeHighlights(highlights) { - for (var i = 0; i < highlights.length; i++) { - if (highlights[i].parentNode) { - var pn = highlights[i].parentNode; - const children = Array.from(highlights[i].childNodes); - replaceWith(highlights[i], children); - pn.normalize(); - } - } + // The `namespaceResolver` and `result` arguments are optional in the spec + // but required in Edge Legacy. + null /* NamespaceResolver */, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null /* Result */ + ).singleNodeValue; + } + } + + /** + * Replace a child `node` with `replacements`. + * + * nb. This is like `ChildNode.replaceWith` but it works in older browsers. + * + * @param {ChildNode} node + * @param {Node[]} replacements + */ + function replaceWith(node, replacements) { + const parent = /** @type {Node} */ (node.parentNode); + + replacements.forEach(r => parent.insertBefore(r, node)); + node.remove(); + } + + /** + * Remove all temporary highlights under a given root element. + */ + function removeAllTempHighlights() { + const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); + if (highlights !== undefined && highlights.length != 0) { + removeHighlights(highlights); + } + } + + /** + * Remove highlights from a range previously highlighted with `highlightRange`. + * + * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange` + */ + function removeHighlights(highlights) { + for (var i = 0; i < highlights.length; i++) { + if (highlights[i].parentNode) { + var pn = highlights[i].parentNode; + const children = Array.from(highlights[i].childNodes); + replaceWith(highlights[i], children); + pn.normalize(); } + } + } - // If user selects text for new annotation - $(document).on('mouseup', '.originaltext', function() { - var selectedrange = window.getSelection().getRangeAt(0); + // If user selects text for new annotation + $(document).on('mouseup', '.originaltext', function () { + var selectedrange = window.getSelection().getRangeAt(0); - if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) { + if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) { - removeAllTempHighlights(); // Remove other temporary highlights. + removeAllTempHighlights(); // Remove other temporary highlights. - resetForms(); // Remove old form contents. + resetForms(); // Remove old form contents. - var entry = this.id.replace(/entry-/, ''); + var entry = this.id.replace(/entry-/, ''); - $('.annotation-form-' + entry + ' input[name="startcontainer"]').val( - xpathFromNode(selectedrange.startContainer, this)); - $('.annotation-form-' + entry + ' input[name="endcontainer"]').val( - xpathFromNode(selectedrange.endContainer, this)); - $('.annotation-form-' + entry + ' input[name="startposition"]').val(selectedrange.startOffset); - $('.annotation-form-' + entry + ' input[name="endposition"]').val(selectedrange.endOffset); + $('.annotation-form-' + entry + ' input[name="startcontainer"]').val( + xpathFromNode(selectedrange.startContainer, this)); + $('.annotation-form-' + entry + ' input[name="endcontainer"]').val( + xpathFromNode(selectedrange.endContainer, this)); + $('.annotation-form-' + entry + ' input[name="startposition"]').val(selectedrange.startOffset); + $('.annotation-form-' + entry + ' input[name="endposition"]').val(selectedrange.endOffset); - $('.annotation-form-' + entry + ' select').val(1); + $('.annotation-form-' + entry + ' select').val(1); - var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp'); + var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp'); - if (annotatedtext != '') { - $('#annotationpreview-temp-' + entry).html(annotatedtext); - } + if (annotatedtext != '') { + $('#annotationpreview-temp-' + entry).html(annotatedtext); + } - $('.annotationarea-' + entry + ' .annotation-form').show(); - $('.annotation-form-' + entry + ' #id_text').focus(); - } - }); - - recreateAnnotations(); - - // Highlight annotation and all annotated text if annotated text is hovered - $('.annotated').mouseenter(function() { - var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).addClass('hovered'); - $('.annotated-' + id).addClass('hovered'); - $('.annotation-box-' + id + ' .errortype').addClass('hovered'); - - }); - - $('.annotated').mouseleave(function() { - var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).removeClass('hovered'); - $('.annotated-' + id).removeClass('hovered'); - $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); - }); - - // Highlight annotated text if annotationpreview is hovered - $('.annotatedtextpreview').mouseenter(function() { - var id = this.id.replace('annotationpreview-', ''); - $('.annotated-' + id).addClass('hovered'); - }); - - $('.annotatedtextpreview').mouseleave(function() { - var id = this.id.replace('annotationpreview-', ''); - $('.annotated-' + id).removeClass('hovered'); - }); - - // Highlight whole temp annotation if part of temp annotation is hovered - $(document).on('mouseover', '.annotated_temp', function() { - $('.annotated_temp').addClass('hovered'); - }); - - $(document).on('mouseleave', '.annotated_temp', function() { - $('.annotated_temp').removeClass('hovered'); - }); - - // Onclick listener for editing annotation. - $(document).on('click', '.annotated', function() { - var id = this.id.replace('annotated-', ''); - editAnnotation(id); - }); - - // Onclick listener for editing annotation. - $(document).on('click', '.edit-annotation', function() { - var id = this.id.replace('edit-annotation-', ''); - editAnnotation(id); - }); - - // Onclick listener for click on annotation-box. - // $(document).on('click', '.annotation-box', function() { - // var id = this.id.replace('annotation-box-', ''); - // $('#annotated-' + id).focus(); - // }); - - // onclick listener if form is canceled - $(document).on('click', '#id_cancel', function(e) { - e.preventDefault(); - - removeAllTempHighlights(); // Remove other temporary highlights. - - resetForms(); // Remove old form contents. - }); - - // Listen for return key pressed to submit annotation form. - $('textarea').keypress(function(e) { - if (e.which == 13) { - $(this).parents(':eq(2)').submit(); - e.preventDefault(); - } - }); + $('.annotationarea-' + entry + ' .annotation-form').show(); + $('.annotation-form-' + entry + ' #id_text').focus(); + } + }); + + recreateAnnotations(); + + // Highlight annotation and all annotated text if annotated text is hovered + $('.annotated').mouseenter(function () { + var id = this.id.replace('annotated-', ''); + $('.annotationpreview-' + id).addClass('hovered'); + $('.annotated-' + id).addClass('hovered'); + $('.annotation-box-' + id + ' .errortype').addClass('hovered'); + + }); + + $('.annotated').mouseleave(function () { + var id = this.id.replace('annotated-', ''); + $('.annotationpreview-' + id).removeClass('hovered'); + $('.annotated-' + id).removeClass('hovered'); + $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); + }); + + // Highlight annotated text if annotationpreview is hovered + $('.annotatedtextpreview').mouseenter(function () { + var id = this.id.replace('annotationpreview-', ''); + $('.annotated-' + id).addClass('hovered'); + }); + + $('.annotatedtextpreview').mouseleave(function () { + var id = this.id.replace('annotationpreview-', ''); + $('.annotated-' + id).removeClass('hovered'); + }); + + // Highlight whole temp annotation if part of temp annotation is hovered + $(document).on('mouseover', '.annotated_temp', function () { + $('.annotated_temp').addClass('hovered'); + }); + + $(document).on('mouseleave', '.annotated_temp', function () { + $('.annotated_temp').removeClass('hovered'); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.annotated', function () { + var id = this.id.replace('annotated-', ''); + editAnnotation(id); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.edit-annotation', function () { + var id = this.id.replace('edit-annotation-', ''); + editAnnotation(id); + }); + + // Onclick listener for click on annotation-box. + // $(document).on('click', '.annotation-box', function() { + // var id = this.id.replace('annotation-box-', ''); + // $('#annotated-' + id).focus(); + // }); + + // onclick listener if form is canceled + $(document).on('click', '#id_cancel', function (e) { + e.preventDefault(); + + removeAllTempHighlights(); // Remove other temporary highlights. + + resetForms(); // Remove old form contents. + + edited = false; + }); + + // Listen for return key pressed to submit annotation form. + $('textarea').keypress(function (e) { + if (e.which == 13) { + $(this).parents(':eq(2)').submit(); + e.preventDefault(); + } + }); }; \ No newline at end of file From 4c45d21cd7a8308e8a0b5677ba5ddd454eb958f7 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Tue, 2 Aug 2022 12:51:56 +0200 Subject: [PATCH 26/60] feat (annotations): added underlines for print view --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 4b8a97f..5c238e4 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,3 +1,3 @@ -define("mod_margic/annotations",["exports","jquery"],(function(_exports,_jquery){var obj;function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i.\n\n/**\n * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (annotations, canmakeannotations, myuserid) => {\n var edited = false;\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function (e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","edited","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","id","style","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"0+CAyBoB,SAACA,YAAaC,mBAAoBC,cAC9CC,QAAS,WA0CJC,eAAeC,iBAEhBF,QAAUE,aACVC,0BACAC,aACAJ,QAAS,OACN,GAAIF,oBAAsBC,UAAYF,YAAYK,cAAcG,OAAQ,CAC3EF,0BACAC,aAEAJ,OAASE,iBAELI,MAAQT,YAAYK,cAAcI,0BAEpC,mBAAqBJ,cAAcK,2BAEnC,oBAAsBD,MAAQ,iCAAiCE,IAAIX,YAAYK,cAAcO,oCAC7F,oBAAsBH,MAAQ,+BAA+BE,IAAIX,YAAYK,cAAcQ,kCAC3F,oBAAsBJ,MAAQ,gCAAgCE,IAAIX,YAAYK,cAAcS,mCAC5F,oBAAsBL,MAAQ,8BAA8BE,IAAIX,YAAYK,cAAcU,iCAE1F,oBAAsBN,MAAQ,+BAA+BE,IAAIN,kCAEjE,oBAAsBI,MAAQ,0BAA0BE,IAAIX,YAAYK,cAAcW,0BAEtF,oBAAsBP,MAAQ,WAAWE,IAAIX,YAAYK,cAAcY,0BAEvE,2BAA6BR,OAAOS,MAAK,mBAAE,sBAAwBb,cAAca,4BACjF,2BAA6BT,OAAOU,IAAI,eAAgB,IAAMnB,YAAYK,cAAce,2BAExF,mBAAqBX,MAAQ,qBAAqBY,aAAa,mBAAqBhB,kCACpF,mBAAqBI,MAAQ,qBAAqBa,2BAClD,mBAAqBb,MAAQ,aAAac,gCAE1C,mBAAqBlB,cAAckB,iBAOpChB,iCACH,oBAAoBG,2BAEpB,gDAAgDC,IAAI,0BAEpD,kDAAkDA,KAAK,uBACvD,gDAAgDA,KAAK,uBACrD,iDAAiDA,KAAK,uBACtD,+CAA+CA,KAAK,uBAEpD,2CAA2CA,IAAI,wBAE/C,mBAAmBa,IAAI,oBAAoBF,gBAYxCG,sBAAsBC,UACvBA,MAAMC,gBAIC,OAIPC,KAAOF,MAAMG,2BACbD,KAAKE,WAAaC,KAAKC,eAMvBJ,KAAOA,KAAKK,gBAEXL,WAGM,WAUPM,KAPEC,UAAY,GACZC,SACFR,KAAKS,cACPC,mBACEV,KACAW,WAAWC,WAGPN,KAAOE,SAASK,eACfC,cAAchB,MAAOQ,WAGtBlB,KAA4BkB,KAE5BlB,OAASU,MAAMiB,gBAAkBjB,MAAMkB,YAAc,EAGrD5B,KAAK6B,UAAUnB,MAAMkB,cAIrB5B,OAASU,MAAMoB,cAAgBpB,MAAMqB,UAAY/B,KAAKgC,KAAKC,QAE3DjC,KAAK6B,UAAUnB,MAAMqB,WAGzBZ,UAAUe,KAAKlC,cAGZmB,mBAaFgB,eAAezB,WAAOrB,qEAAsB+C,gEAAW,YAAahC,6DAAQ,SAE3Ee,UAAYV,sBAAsBC,OAIpC2B,cAAgB,GAChBC,SAAW,KACXC,YAAc,KAElBpB,UAAUqB,SAAQ,SAAAtB,MACVoB,UAAYA,SAASG,cAAgBvB,KACrCqB,YAAYL,KAAKhB,OAEjBqB,YAAc,CAACrB,MACfmB,cAAcH,KAAKK,cAEvBD,SAAWpB,YAMTwB,WAAa,QACnBL,cAAgBA,cAAcM,QAAO,SAAAC,aAEjCA,KAAKC,MAAK,SAAA3B,aAASwB,WAAWI,KAAK5B,KAAK6B,qBAIxCC,gBAAkB,UAEtBX,cAAcG,SAAQ,SAAAS,WACZC,YAAcC,SAASC,cAAc,QAC3CF,YAAYG,UAAYjB,SAEpB/C,eACA6D,YAAYG,WAAa,IAAMjB,SAAW,IAAM/C,aAEhD6D,YAAYI,GAAKlB,SAAW,IAAM/C,aAClC6D,YAAYK,MAAMC,gBAAkB,IAAMpD,OAG9C4C,iBAAmBC,MAAM,GAAGQ,YAE5BR,MAAM,GAAGS,WAAWC,aAAaT,YAAaD,MAAM,IACpDA,MAAMT,SAAQ,SAAAtB,aAAQgC,YAAYU,YAAY1C,YAI3C8B,yBAUFtB,cAAchB,MAAOQ,oDAEhBe,6DAASf,KAAK6B,4CAALc,gBAAgB5B,8DAAUf,KAAK4C,WAAW7B,cAGrDvB,MAAMqD,aAAa7C,KAAM,IAAM,GAE/BR,MAAMqD,aAAa7C,KAAMe,SAAW,EAE1C,MAAO+B,UAGE,YA4CNC,eAAe/C,UACdgD,cAnCWhD,UACXiD,SAAWjD,KAAKiD,SAASC,cAC3BC,OAASF,eACI,UAAbA,WACAE,OAAS,UAENA,OA6BMC,CAAYpD,MACnBqD,aArBerD,cACjBqD,IAAM,EAENC,IAAMtD,KACHsD,KACCA,IAAIL,WAAajD,KAAKiD,WACtBI,KAAO,GAEXC,IAAMA,IAAIC,uBAEPF,IAWKG,CAAgBxD,sBAClBgD,iBAAQK,kBAWbI,cAAczD,KAAMN,cACrBgE,MAAQ,GAGRC,KAAO3D,KACJ2D,OAASjE,MAAM,KACbiE,WACK,IAAIC,MAAM,oCAEpBF,MAAQX,eAAeY,MAAQ,IAAMD,MACrCC,KAAOA,KAAKnB,kBAGhBkB,OADAA,MAAQ,IAAMA,OACAG,QAAQ,MAAO,aAcxBC,eAAeC,QAASd,SAAUe,OACvCf,SAAWA,SAASgB,sBAEhBC,YAAc,EACTC,EAAI,EAAGA,EAAIJ,QAAQK,SAASrD,OAAQoD,IAAK,KACxCE,MAAQN,QAAQK,SAASD,MAC3BE,MAAMpB,SAASgB,gBAAkBhB,YAC/BiB,aACiBF,aACRK,aAKZ,cAwBFC,oBAAoBZ,MAAOhE,WAC2C,OAArDgE,MAAMa,MAAM,4CAExB,IAAIX,MAAM,wCAGdY,SAAWd,MAAMe,MAAM,KACzBV,QAAUrE,KAId8E,SAASE,uDAEWF,6DAAU,KAArBG,oBACDC,mBACAC,oBAEEC,aAAeH,QAAQI,QAAQ,SACf,IAAlBD,aAAqB,CACrBF,YAAcD,QAAQK,MAAM,EAAGF,kBAEzBG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,UACjEF,aAAeK,SAASD,UAAY,GACjB,SACR,UAGXL,YAAcD,QACdE,aAAe,MAGbR,MAAQP,eAAeC,QAASa,YAAaC,kBAC9CR,aACM,KAGXN,QAAUM,gEAGPN,iBAaFoB,cAAczB,WAAOhE,4DAAOuC,SAASmD,gBAE/Bd,oBAAoBZ,MAAOhE,MACpC,MAAO2F,YACEpD,SAASqD,SACZ,IAAM5B,MACNhE,KAIA,KACA6F,YAAYC,wBACZ,MACFC,0BAYDC,YAAY1F,KAAM2F,kBACjBC,OAA8B5F,KAAKwC,WAEzCmD,aAAarE,SAAQ,SAAAuE,UAAKD,OAAOzG,aAAa0G,EAAG7F,SACjDA,KAAK8F,kBAMA1H,8BACC2H,WAAaC,MAAMC,MAAK,mBAAE,QAAQ,GAAGC,iBAAiB,yBACzCC,IAAfJ,YAAiD,GAArBA,WAAWhF,iBAUrBgF,gBACjB,IAAI5B,EAAI,EAAGA,EAAI4B,WAAWhF,OAAQoD,OAC/B4B,WAAW5B,GAAG3B,WAAY,KACtB4D,GAAKL,WAAW5B,GAAG3B,WACjB4B,SAAW4B,MAAMC,KAAKF,WAAW5B,GAAGvB,YAC1C8C,YAAYK,WAAW5B,GAAIC,UAC3BgC,GAAGC,aAfPC,CAAiBP,gCAxcvB,oBAAoBvH,2BAGpB,iCAAiC+H,YAAY,gCAC7C,iCAAiCA,YAAY,gCAC7C,mCAAmCA,YAAY,kCAC/C,4BAA4BA,YAAY,2BAudxCtE,UAAUuE,GAAG,UAAW,iBAAiB,eACnCC,cAAgBC,OAAOC,eAAeC,WAAW,MAEH,KAA9CH,cAAcI,gBAAgBtE,aAAsBxE,mBAAoB,CAExEK,0BAEAC,iBAEIE,MAAQuI,KAAK1E,GAAGyB,QAAQ,SAAU,wBAEpC,oBAAsBtF,MAAQ,iCAAiCE,IAC7DgF,cAAcgD,cAAchG,eAAgBqG,2BAC9C,oBAAsBvI,MAAQ,+BAA+BE,IAC3DgF,cAAcgD,cAAc7F,aAAckG,2BAC5C,oBAAsBvI,MAAQ,gCAAgCE,IAAIgI,cAAc/F,iCAChF,oBAAsBnC,MAAQ,8BAA8BE,IAAIgI,cAAc5F,+BAE9E,oBAAsBtC,MAAQ,WAAWE,IAAI,OAE3CsI,cAAgB9F,eAAewF,eAAe,EAAO,kBAEpC,IAAjBM,mCACE,2BAA6BxI,OAAOS,KAAK+H,mCAG7C,mBAAqBxI,MAAQ,qBAAqBa,2BAClD,oBAAsBb,MAAQ,aAAac,mDA3e1B2H,OAAOC,OAAOnJ,2CAAc,KAA1CoJ,8BAGDC,SAAWlF,SAASmF,kBAGpBD,SAASE,SACLlC,cAAc+B,WAAWxI,gBAAgB,mBAAE,UAAYwI,WAAW3I,OAAO,IAAK2I,WAAWtI,eAC7FuI,SAASG,OACLnC,cAAc+B,WAAWvI,cAAc,mBAAE,UAAYuI,WAAW3I,OAAO,IAAK2I,WAAWrI,aAC7F,MAAOiE,QAILiE,cAAgB9F,eAAekG,SAAUD,WAAW9E,GAAI,YAAa8E,WAAWhI,OAE/D,IAAjB6H,mCACE,sBAAwBG,WAAW9E,IAAIpD,KAAK+H,gBA8d1DQ,uBAGE,cAAcC,YAAW,eACnBpF,GAAK0E,KAAK1E,GAAGyB,QAAQ,aAAc,wBACrC,sBAAwBzB,IAAIqF,SAAS,+BACrC,cAAgBrF,IAAIqF,SAAS,+BAC7B,mBAAqBrF,GAAK,eAAeqF,SAAS,kCAItD,cAAcC,YAAW,eACnBtF,GAAK0E,KAAK1E,GAAGyB,QAAQ,aAAc,wBACrC,sBAAwBzB,IAAImE,YAAY,+BACxC,cAAgBnE,IAAImE,YAAY,+BAChC,mBAAqBnE,GAAK,eAAemE,YAAY,kCAIzD,yBAAyBiB,YAAW,eAC9BpF,GAAK0E,KAAK1E,GAAGyB,QAAQ,qBAAsB,wBAC7C,cAAgBzB,IAAIqF,SAAS,kCAGjC,yBAAyBC,YAAW,eAC9BtF,GAAK0E,KAAK1E,GAAGyB,QAAQ,qBAAsB,wBAC7C,cAAgBzB,IAAImE,YAAY,kCAIpCtE,UAAUuE,GAAG,YAAa,mBAAmB,+BACzC,mBAAmBiB,SAAS,kCAGhCxF,UAAUuE,GAAG,aAAc,mBAAmB,+BAC1C,mBAAmBD,YAAY,kCAInCtE,UAAUuE,GAAG,QAAS,cAAc,WAElCtI,eADS4I,KAAK1E,GAAGyB,QAAQ,aAAc,4BAKzC5B,UAAUuE,GAAG,QAAS,oBAAoB,WAExCtI,eADS4I,KAAK1E,GAAGyB,QAAQ,mBAAoB,4BAW/C5B,UAAUuE,GAAG,QAAS,cAAc,SAAU1D,GAC5CA,EAAE6E,iBAEFvJ,0BAEAC,aAEAJ,QAAS,yBAIX,YAAY2J,UAAS,SAAU9E,GACd,IAAXA,EAAE+E,4BACAf,MAAMgB,QAAQ,UAAUC,SAC1BjF,EAAE6E"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (annotations, canmakeannotations, myuserid) => {\n var edited = false;\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function (e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","edited","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 923d817..0a29954 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -236,6 +236,7 @@ export const init = (annotations, canmakeannotations, myuserid) => { if (annotationid) { highlightEl.className += ' ' + cssClass + '-' + annotationid; // highlightEl.tabIndex = 1; + highlightEl.style = "text-decoration:underline; text-decoration-color: #" + color; highlightEl.id = cssClass + '-' + annotationid; highlightEl.style.backgroundColor = '#' + color; } From 4955395ca04665ed0d64263af0b35b87a68ad45a Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 3 Aug 2022 15:31:33 +0200 Subject: [PATCH 27/60] fix (annotations): fix for sorting of annotations (still not fully working) --- locallib.php | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/locallib.php b/locallib.php index bf248c9..aac7500 100644 --- a/locallib.php +++ b/locallib.php @@ -485,7 +485,10 @@ public function get_sortmode() { private function index_original($doc) { // var_dump('index_original: $doc'); + + // echo '
';
         // var_dump($doc);
+        // echo '
'; foreach ($doc->childNodes as $childnode) { // var_dump('index_original: $childnode'); @@ -607,8 +610,20 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode $this->index_original($doc); - // var_dump('NEW ENTRY'); - // var_dump($i); + // echo '
';
+        // var_dump('Text');
+        // var_dump(htmlspecialchars($entry->text));
+        // echo '
'; + + // echo '
';
+            // foreach ($this->nodepositions as $position => $node) {
+            //     if (isset($node->tagName)) {
+            //         echo $position . ' ' . $node->tagName . "\n";
+            //     } else {
+            //         echo $position . ' ' . $node->nodeValue . "\n";
+            //     }
+            // }
+        // echo '
'; // var_dump('
'); // var_dump('
'); @@ -672,16 +687,83 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode // var_dump('$nodepositions'); // var_dump($this->nodepositions); + // echo '
';
+            $found = false;
 
             foreach ($this->nodepositions as $position => $node) {
+                // if ($annotation->text == 'sadipscing') {
+                //     var_dump($annotation->startcontainer);
+                //     var_dump($nodelist[0]);
+                //     var_dump($node);
+                // }
+
                 if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array.
+                    $found = true;
                     $entry->annotations[$key]->position = $position; // If so asssign its position to annotation.
                     // echo "POSITION OF ANNOTATION:  
"; // echo $entry->annotations[$key]->position; // echo "
"; + + // echo 'position:' . $position . " \n"; + // echo $annotation->text . " \n"; + // echo 'offset:' . $annotation->startposition . " \n"; + // echo " \n\n"; + + // echo 'HIGHLIHGHT'; + // $span = $doc->createElement('span', htmlspecialchars(substr($nodelist[0]->textContent, $annotation->startposition))); + // var_dump($doc->saveXml($span)); break; } } + + if (!$found) { + // echo 'Ich darf nicht passieren!!!!'; + $diffoffsets = 0; + foreach ($entry->annotations as $key2 => $annotation2) { + $xpathprefix1 = substr($annotation->startcontainer, 0, -3); + $xpathprefix2 = substr($annotation2->startcontainer, 0, -3); + + if ($key2 >= $key) { + break; + } + + if ($xpathprefix2 == $xpathprefix1) { + $diffoffsets += $annotation2->startposition; + } + } + + $xpath = new DOMXpath($doc); + $fixedxpath = substr($annotation->startcontainer, 0, -3) . '[1]'; + $nodelist = $xpath->query('/' . $fixedxpath); + + // echo 'NOT FOUND' . " \n"; + // var_dump($annotation->startcontainer); + // var_dump('/' . $fixedxpath); + // var_dump($nodelist[0]); + // var_dump($node); + + foreach ($this->nodepositions as $position => $node) { + + if ($nodelist[0] === $node) { // Check if startcontainer node ($nodelist[0]) is same as node in nodepositions array. + $found = true; + $entry->annotations[$key]->position = $position; // If so asssign its position to annotation. + // echo "POSITION OF ANNOTATION:
"; + // echo $entry->annotations[$key]->position; + // echo "
"; + + // echo 'position fixed:' . $position . " \n"; + // echo $annotation->text . " \n"; + // echo 'offset:' . $annotation->startposition . " \n"; + // echo " \n\n"; + //$annotation->startposition += $diffoffsets; + break; + } + + } + } + + // echo '
'; + } // Sort annotations by position and offset of startcontainer. From 69c7928b420c38cdb833251250d19702fba02d83 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 3 Aug 2022 15:45:11 +0200 Subject: [PATCH 28/60] feat (grading): now sending messages to users if entry is graded --- classes/privacy/provider.php | 1 + db/access.php | 14 ++- db/messages.php | 37 +++++++ grade_entry.php | 43 +++++++- grading_form.php | 15 ++- lang/de/margic.php | 30 +++--- lang/en/margic.php | 30 +++--- lib.php | 189 ----------------------------------- styles.css | 1 - version.php | 2 +- 10 files changed, 137 insertions(+), 225 deletions(-) create mode 100644 db/messages.php diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index d6f83b5..25dd756 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -93,6 +93,7 @@ public static function get_metadata(collection $items): collection { // The margic uses multiple subsystems that save personal data. $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); $items->add_subsystem_link('core_rating', [], 'privacy:metadata:core_rating'); + $items->add_subsystem_link('core_message', [], 'privacy:metadata:core_message'); // User preferences in the margic. $items->add_user_preference('sortoption', 'privacy:metadata:preference:sortoption'); diff --git a/db/access.php b/db/access.php index 60353c7..e12b5b5 100644 --- a/db/access.php +++ b/db/access.php @@ -100,5 +100,17 @@ 'archetypes' => array( 'manager' => CAP_ALLOW ) - ) + ), + + 'mod/margic:receivegradingmessages' => array( + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), ); diff --git a/db/messages.php b/db/messages.php new file mode 100644 index 0000000..fb480cb --- /dev/null +++ b/db/messages.php @@ -0,0 +1,37 @@ +. + +/** + * Plugin message providers are defined here. + * + * @package mod_margic + * @category message + * @copyright 2022 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$messageproviders = array ( + + 'gradingmessages' => array( + 'capability' => 'mod/margic:receivegradingmessages', + 'defaults' => array( + 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, + 'email' => MESSAGE_PERMITTED, + ), + ), +); diff --git a/grade_entry.php b/grade_entry.php index 7cfa6fb..a7fc833 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -65,6 +65,8 @@ require_capability('mod/margic:addentries', $context); +$PAGE->set_url('/mod/margic/grade_entry.php', array('id' => $id)); + $entry = $DB->get_record('margic_entries', array('id' => $entryid, 'margic' => $cm->instance)); $grades = make_grades_menu($moduleinstance->scale); @@ -84,7 +86,8 @@ $data->{'rating_' . $entry->id} = $entry->rating; // Instantiate gradingform and save submitted data if it exists. -$mform = new \mod_margic_grading_form(null, array('courseid' => $course->id, 'margic' => $moduleinstance, 'entry' => $entry, 'grades' => $grades, 'teacherimg' => '', 'editoroptions' => $editoroptions)); +$mform = new \mod_margic_grading_form(null, + array('courseid' => $course->id, 'margic' => $moduleinstance, 'entry' => $entry, 'grades' => $grades, 'teacherimg' => '', 'editoroptions' => $editoroptions)); $mform->set_data($data); @@ -179,6 +182,44 @@ $event->add_record_snapshot('margic', $moduleinstance); $event->trigger(); + if ($fromform->sendgradingmessage) { + global $CFG; + + $url = (new \moodle_url("/mod/margic/view.php?id=$id"))->out(false); + + $obj = new stdClass(); + $obj->user = fullname(core_user::get_user($entry->userid)); + $obj->teacher = fullname(core_user::get_user($entry->teacher)); + $obj->margic = format_string($moduleinstance->name, true); + $obj->url = $url; + + // Send grading message. + $message = new \core\message\message(); + $message->component = 'mod_margic'; + $message->name = 'gradingmessages'; + $message->userfrom = core_user::get_noreply_user(); // If the message is 'from' a specific user you can set them here + $message->userto = $entry->userid; + $message->subject = get_string('gradingmailsubject', 'mod_margic'); + $message->fullmessage = get_string('gradingmailfullmessage', 'mod_margic', $obj); + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '

' . get_string('gradingmailfullmessagehtml', 'mod_margic', $obj) . '

'; + $message->smallmessage = get_string('gradingmailfullmessage', 'mod_margic', $obj); + $message->notification = 1; // Because this is a notification generated from Moodle, not a user-to-user message + $message->contexturl = $url; // A relevant URL for the notification + $message->contexturlname = get_string('modulename', 'mod_margic') + . ' - ' . get_string('overview', 'mod_margic'); // Link title explaining where users get to for the contexturl + + $header = ''; + $urllink = '' . $url . ''; + $footer = '

---------------------------------------------------------------------
' . get_string('mailfooter', 'mod_margic', + ['systemname' => get_config('shortname'), 'coursename' => $course->fullname, 'name' => $moduleinstance->name, 'url' => $url]); + $content = array('*' => array('header' => $header, 'footer' => $footer)); // Extra content for specific processor + $message->set_additional_content('email', $content); + + // Actually send the message + $messageid = message_send($message); + } + // Redirect after updated from feedback and grades. redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('feedbackupdated', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { diff --git a/grading_form.php b/grading_form.php index 1eb110f..bf4b653 100644 --- a/grading_form.php +++ b/grading_form.php @@ -80,9 +80,11 @@ public function definition() { $feedbackdisabled = true; - $gradebooklinkrating = '' . $gradinginfo->items[0]->grades[$this->_customdata['entry']->userid]->str_long_grade . ''; + $gradebooklinkrating = '' . $gradinginfo->items[0]->grades[$this->_customdata['entry']->userid]->str_long_grade . ''; - $gradebooklinkfeedback = '' . $gradinginfo->items[0]->grades[$this->_customdata['entry']->userid]->str_feedback . ''; + $gradebooklinkfeedback = '' . $gradinginfo->items[0]->grades[$this->_customdata['entry']->userid]->str_feedback . ''; $attr = array('disabled' => 'disabled'); } @@ -95,7 +97,8 @@ public function definition() { $mform->addElement('html', '
'); if ($this->_customdata['entry']->timemarked) { - $mform->addElement('static', 'currentuserrating', get_string('grader', 'mod_margic'), $this->_customdata['teacherimg'] . ' - ' . userdate($this->_customdata['entry']->timemarked)); + $mform->addElement('static', 'currentuserrating', + get_string('grader', 'mod_margic'), $this->_customdata['teacherimg'] . ' - ' . userdate($this->_customdata['entry']->timemarked)); $mform->addElement('static', 'savedrating', get_string('savedrating', 'mod_margic'), $this->_customdata['entry']->rating); } @@ -113,9 +116,13 @@ public function definition() { $mform->addElement('static', 'gradebookfeedback', get_string('feedbackingradebook', 'margic') . ': ', $gradebooklinkfeedback); } else { - $mform->addElement('editor', 'feedback_' . $this->_customdata['entry']->id . '_editor', get_string('entrycomment', 'mod_margic'), null, $this->_customdata['editoroptions']); + $mform->addElement('editor', 'feedback_' . $this->_customdata['entry']->id . '_editor', + get_string('entrycomment', 'mod_margic'), null, $this->_customdata['editoroptions']); $mform->setType('feedback_' . $this->_customdata['entry']->id . '_editor', PARAM_RAW); + $mform->addElement('selectyesno', 'sendgradingmessage', get_string('sendgradingmessage', 'margic')); + $mform->setDefault('sendgradingmessage', 1); + $this->add_action_buttons(); } diff --git a/lang/de/margic.php b/lang/de/margic.php index 77456af..7246529 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -42,20 +42,12 @@ $string['margic:addinstance'] = 'Margic-Instanzen hinzufügen'; $string['margic:manageentries'] = 'Margic-Einträge verwalten'; $string['margic:rate'] = 'Margic-Einträge bewerten'; +$string['margic:receivegradingmessages'] = 'Nachrichten über die Bewertung von Einträgen erhalten'; +$string['margic:editdefaulterrortypes'] = 'Standardfehlertyp-Vorlagen bearbeiten'; $string['margicclosetime'] = 'Endzeitpunkt'; $string['margicclosetime_help'] = 'Wenn diese Option aktiviert ist können Sie ein Datum festlegen, an dem die Margic-Instanz geschlossen wird. Teilnehmende können danach keine Einträge mehr anlegen oder bearbeiten.'; $string['margicdescription'] = 'Beschreibung der Margic-Instanz'; $string['margicentrydate'] = 'Datum für diesen Eintrag festlegen'; -$string['margicmail'] = 'Hallo {$a->user}, -{$a->teacher} hat einige Rückmeldungen zu Ihrem Margic-Eintrag für \'{$a->margic}\' veröffentlicht. - -Sie können diese als Anhang zu Ihrem Margic-Eintrag sehen: - - {$a->url}'; -$string['margicmailhtml'] = 'Hallo {$a->user}, -{$a->teacher} hat einige Rückmeldungen zu Ihrem -Margic-Eintrag für \'{$a->margic}\' veröffentlicht.

-Sie können diese als Anhang zu Ihrem Margic-Eintrag sehen >/a>.'; $string['margicname'] = 'Name der Margic-Instanz'; $string['margicdescription'] = 'Beschreibung der Margic-Instanz'; $string['margicopentime'] = 'Startzeit'; @@ -93,8 +85,6 @@ $string['feedbackingradebook'] = 'Aktuelles Feedback aus der Bewertungsübersicht'; $string['lastnameasc'] = 'Nachname aufsteigend:'; $string['lastnamedesc'] = 'Nachname absteigend:'; -$string['mailed'] = 'Benachrichtigt'; -$string['mailsubject'] = 'Rückmeldung zur Margic-Instanz'; $string['modulename'] = 'Margic'; $string['modulename_help'] = 'Die Margic-Instanz kann tolle Dinge ...'; $string['modulenameplural'] = 'Margics'; @@ -269,6 +259,17 @@ $string['from'] = 'von'; $string['toggleolderversions'] = 'Ältere Versionen ein- oder ausblenden'; $string['timecreatedinvalid'] = 'Änderung fehlgeschlagen. Es gibt bereits jüngere Versionen dieses Beitrags.'; +$string['messageprovider:gradingmessages'] = 'Systemnachrichten bei der Bewertung von Einträgen'; +$string['sendgradingmessage'] = 'Ersteller/in des Eintrags sofort über die Bewertung benachrichtigen'; +$string['mailed'] = 'Benachrichtigt'; +$string['gradingmailsubject'] = 'Feedback zu Margic-Eintrag erhalten'; +$string['gradingmailfullmessage'] = 'Hallo {$a->user}, +{$a->teacher} hat eine Rückmeldung beziehungsweise Bewertung zu Ihrem Eintrag im Margic {$a->margic} veröffentlicht. +Hier können Sie diese ansehen: {$a->url}'; +$string['gradingmailfullmessagehtml'] = 'Hallo {$a->user},
+{$a->teacher} hat eine Rückmeldung beziehungsweise Bewertung zu Ihrem Eintrag im Margic {$a->margic} veröffentlicht.

+
Hier können Sie diese ansehen.'; +$string['mailfooter'] = 'Diese Nachricht bezieht sich auf ein Margic in {$a->systemname}. Unter dem folgenden Link finden Sie alle weiteren Informationen.
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; @@ -296,8 +297,9 @@ $string['privacy:metadata:margic_errortype_templates:name'] = 'Name der Fehlertyp-Vorlage.'; $string['privacy:metadata:margic_errortype_templates:color'] = 'Farbe der Fehlertyp-Vorlage als Hex-Wert.'; $string['privacy:metadata:margic_errortype_templates:userid'] = 'ID des Benutzers, der die Fehlertyp-Vorlage erstellt hat.'; -$string['privacy:metadata:core_rating'] = 'Die zu den Margic-Einträgen hinzugefügten Bewertungen werden unter Verwendung des core_rating-Systems gespeichert.'; -$string['privacy:metadata:core_files'] = 'Dateien, die mit Margic-Einträgen verknüpft sind, werden mithilfe des Systems core_files gespeichert.'; +$string['privacy:metadata:core_rating'] = 'Es werden zu Margic-Einträgen hinzugefügte Bewertungen gespeichert.'; +$string['privacy:metadata:core_files'] = 'Es werden mit Margic-Einträgen verknüpfte Dateien gespeichert.'; +$string['privacy:metadata:core_message'] = 'Es werden Nachrichten über die Bewertung von Margic-Einträgen an Benutzer versendet.'; $string['privacy:metadata:preference:sortoption'] = 'Die Präferenz für die Sortierung jedes Margics.'; $string['privacy:metadata:preference:margic_pagecount'] = 'Die Anzahl der Einträge, die pro Seite in jedem Margic angezeigt werden sollen.'; $string['privacy:metadata:preference:margic_activepage'] = 'Die Nummer der aktuell geöffneten Seite in jedem Margic.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 6036968..97ce28b 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -81,16 +81,8 @@ $string['margic:addinstance'] = 'Add margic instances'; $string['margic:manageentries'] = 'Manage margic entries'; $string['margic:rate'] = 'Rate margic entries'; -$string['margicmail'] = 'Greetings {$a->user}, -{$a->teacher} has posted some feedback on your margic entry for \'{$a->margic}\'. - -You can see it appended to your margic entry: - - {$a->url}'; -$string['margicmailhtml'] = 'Greetings {$a->user}, -{$a->teacher} has posted some feedback on your -margic entry for \'{$a->margic}\'.

-You can see it appended to your margic entry.'; +$string['margic:receivegradingmessages'] = 'Receive messages about the rating of entries'; +$string['margic:editdefaulterrortypes'] = 'Edit default error type templates'; $string['margicname'] = 'Name of the margic'; $string['margicdescription'] = 'Description of the margic'; $string['format'] = 'Format'; @@ -106,8 +98,6 @@ $string['lastnameasc'] = 'Last name ascending:'; $string['lastnamedesc'] = 'Last name descending:'; -$string['mailed'] = 'Mailed'; -$string['mailsubject'] = 'margic feedback'; $string['modulename'] = 'Margic'; $string['modulename_help'] = 'The margic activity enables teachers to obtain students feedback over a period of time.'; @@ -282,6 +272,17 @@ $string['from'] = 'from'; $string['toggleolderversions'] = 'Toggle older versions of the entry'; $string['timecreatedinvalid'] = 'Change failed. There are already younger versions of this entry.'; +$string['messageprovider:gradingmessages'] = 'Notifications when entries are rated'; +$string['sendgradingmessage'] = 'Notify the creator of the entry immediately about the rating'; +$string['mailed'] = 'Mailed'; +$string['gradingmailsubject'] = 'Received feedback for Margic entry'; +$string['gradingmailfullmessage'] = 'Greetings {$a->user}, +{$a->teacher} has published a feedback or rating for your entry in Margic {$a->margic}. +Here you can view them: {$a->url}'; +$string['gradingmailfullmessagehtml'] = 'Greetings {$a->user},
+{$a->teacher} has published a feedback or rating for your entry in Margic {$a->margic}.

+Here you can view them.'; +$string['mailfooter'] = 'This message is about a Margic in {$a->systemname}. You can find all further information under the following link:
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; @@ -309,8 +310,9 @@ $string['privacy:metadata:margic_errortype_templates:name'] = 'Name of the errortype template.'; $string['privacy:metadata:margic_errortype_templates:color'] = 'Color of the errortype template as hex value.'; $string['privacy:metadata:margic_errortype_templates:userid'] = 'ID of the user that made the errortype template.'; -$string['privacy:metadata:core_rating'] = 'Ratings added to margic entries are stored using the core_rating system.'; -$string['privacy:metadata:core_files'] = 'Files linked to margic entries are stored using the core_files system.'; +$string['privacy:metadata:core_rating'] = 'Ratings added to Margic entries are saved.'; +$string['privacy:metadata:core_files'] = 'Files associated with Margic entries are saved.'; +$string['privacy:metadata:core_message'] = 'Messages are sent to users about the grading of Margic entries.'; $string['privacy:metadata:preference:sortoption'] = 'The preference for the sorting of each margic.'; $string['privacy:metadata:preference:margic_pagecount'] = 'The number of entries that should be shown per page for each Margic.'; $string['privacy:metadata:preference:margic_activepage'] = 'The number of the page currently opened in each Margic.'; diff --git a/lib.php b/lib.php index dff5669..ae8a704 100644 --- a/lib.php +++ b/lib.php @@ -330,127 +330,6 @@ function margic_user_complete($course, $user, $mod, $margic) { } } -/** - * Function to be run periodically according to the moodle cron. - * Finds all margic notifications that have yet to be mailed out, and mails them. - * - * @return boolean True if successful. - */ -/* function margic_cron() { - global $CFG, $USER, $DB; - - $cutofftime = time() - $CFG->maxeditingtime; - - if ($entries = margic_get_unmailed_graded($cutofftime)) { - $timenow = time(); - - $usernamefields = get_all_user_name_fields(); - $requireduserfields = 'id, auth, mnethostid, email, mailformat, maildisplay, lang, deleted, suspended, ' - .implode(', ', $usernamefields); - - // To save some db queries. - $users = array(); - $courses = array(); - - foreach ($entries as $entry) { - - echo "Processing margic entry $entry->id\n"; - - if (! empty($users[$entry->userid])) { - $user = $users[$entry->userid]; - } else { - if (! $user = $DB->get_record("user", array( - "id" => $entry->userid - ), $requireduserfields)) { - echo "Could not find user $entry->userid\n"; - continue; - } - $users[$entry->userid] = $user; - } - - $USER->lang = $user->lang; - - if (! empty($courses[$entry->course])) { - $course = $courses[$entry->course]; - } else { - if (! $course = $DB->get_record('course', array( - 'id' => $entry->course), 'id, shortname')) { - echo "Could not find course $entry->course\n"; - continue; - } - $courses[$entry->course] = $course; - } - - if (! empty($users[$entry->teacher])) { - $teacher = $users[$entry->teacher]; - } else { - if (! $teacher = $DB->get_record("user", array( - "id" => $entry->teacher), $requireduserfields)) { - echo "Could not find teacher $entry->teacher\n"; - continue; - } - $users[$entry->teacher] = $teacher; - } - - // All cached. - $coursemargics = get_fast_modinfo($course)->get_instances_of('margic'); - if (empty($coursemargics) || empty($coursemargics[$entry->margic])) { - echo "Could not find course module for margic id $entry->margic\n"; - continue; - } - $mod = $coursemargics[$entry->margic]; - - // This is already cached internally. - $context = context_module::instance($mod->id); - $canadd = has_capability('mod/margic:addentries', $context, $user); - $entriesmanager = has_capability('mod/margic:manageentries', $context, $user); - - if (! $canadd and $entriesmanager) { - continue; // Not an active participant. - } - - $margicinfo = new stdClass(); - // 20200829 Added users first and last name to message. - $margicinfo->user = $user->firstname . ' ' . $user->lastname; - $margicinfo->teacher = fullname($teacher); - $margicinfo->margic = format_string($entry->name, true); - $margicinfo->url = "$CFG->wwwroot/mod/margic/view.php?id=$mod->id"; - $modnamepl = get_string('modulenameplural', 'margic'); - $msubject = get_string('mailsubject', 'margic'); - - $postsubject = "$course->shortname: $msubject: " . format_string($entry->name, true); - $posttext = "$course->shortname -> $modnamepl -> " . format_string($entry->name, true) . "\n"; - $posttext .= "---------------------------------------------------------------------\n"; - $posttext .= get_string("margicmail", "margic", $margicinfo) . "\n"; - $posttext .= "---------------------------------------------------------------------\n"; - if ($user->mailformat == 1) { // HTML. - $posthtml = "

" - ."wwwroot/course/view.php?id=$course->id\">$course->shortname ->" - ."wwwroot/mod/margic/index.php?id=$course->id\">margics ->" - ."wwwroot/mod/margic/view.php?id=$mod->id\">" - .format_string($entry->name, true) - ."

"; - $posthtml .= "
"; - $posthtml .= "

" . get_string("margicmailhtml", "margic", $margicinfo) . "

"; - $posthtml .= "

"; - } else { - $posthtml = ""; - } - - if (! email_to_user($user, $teacher, $postsubject, $posttext, $posthtml)) { - echo "Error: margic cron: Could not send out mail for id $entry->id to user $user->id ($user->email)\n"; - } - if (! $DB->set_field("margic_entries", "mailed", "1", array( - "id" => $entry->id - ))) { - echo "Could not update the mailed field for id $entry->id\n"; - } - } - } - - return true; -} */ - /** * Given a course and a time, this module should find recent activity * that has occurred in margic activities and print it out. @@ -921,74 +800,6 @@ function margic_scale_used_anywhere($scaleid) { return $margics; } */ -/** - * Counts all the margic entries (optionally in a given group). - * - * @param array $margic - * @param int $groupid - * @return int count($margics) Count of margic entries. - */ -/* function margic_count_entries($margic, $groupid = 0) { - global $DB; - - $cm = margic_get_coursemodule($margic->id); - $context = context_module::instance($cm->id); - - if ($groupid) { // How many in a particular group? - - $sql = "SELECT DISTINCT u.id FROM {margic_entries} d - JOIN {groups_members} g ON g.userid = d.userid - JOIN {user} u ON u.id = g.userid - WHERE d.margic = ? AND g.groupid = ?"; - $margics = $DB->get_records_sql($sql, array( - $margic->id, - $groupid - )); - } else { // Count all the entries from the whole course. - - $sql = "SELECT DISTINCT u.id FROM {margic_entries} d - JOIN {user} u ON u.id = d.userid - WHERE d.margic = ?"; - $margics = $DB->get_records_sql($sql, array( - $margic->id - )); - } - - if (! $margics) { - return 0; - } - - $canadd = get_users_by_capability($context, 'mod/margic:addentries', 'u.id'); - $entriesmanager = get_users_by_capability($context, 'mod/margic:manageentries', 'u.id'); - - // Remove unenrolled participants. - foreach ($margics as $userid => $notused) { - - if (! isset($entriesmanager[$userid]) && ! isset($canadd[$userid])) { - unset($margics[$userid]); - } - } - - return count($margics); -} */ - -/** - * Return entries that have not been emailed. - * - * @param int $cutofftime - * @return object - */ -/* function margic_get_unmailed_graded($cutofftime) { - global $DB; - - $sql = "SELECT de.*, d.course, d.name FROM {margic_entries} de - JOIN {margic} d ON de.margic = d.id - WHERE de.mailed = '0' AND de.timemarked < ? AND de.timemarked > 0"; - return $DB->get_records_sql($sql, array( - $cutofftime - )); -} */ - /** * Return margic log info. * diff --git a/styles.css b/styles.css index b905dc7..1fea61d 100644 --- a/styles.css +++ b/styles.css @@ -90,7 +90,6 @@ text-align: left; font-size: 1em; padding: 10px; - padding-bottom: 0px; border: 1px solid rgba(0, 0, 0, .125); border-radius: 5px; -webkit-border-radius: 5px; diff --git a/version.php b/version.php index 747a768..0212cd6 100644 --- a/version.php +++ b/version.php @@ -26,6 +26,6 @@ $plugin->component = 'mod_margic'; $plugin->release = '1.1.3'; // User-friendly version number. -$plugin->version = 2022072600; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2022080100; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_BETA; From d92a6ec05af16f85ff9dde86e61dc4b7cc5b1565 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 3 Aug 2022 16:36:55 +0200 Subject: [PATCH 29/60] feat (view): minor adjustments for print view --- classes/output/margic_view.php | 1 - lang/de/margic.php | 2 +- lang/en/margic.php | 2 +- lib.php | 1 - mod_form.php | 7 +++++++ styles.css | 26 ++++++++++++++++++++++---- 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 9b039f6..55bb432 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -203,7 +203,6 @@ public function export_for_template(renderer_base $output) { } } - $data->entries = $this->entries; $data->sortmode = $this->sortmode; $data->entrybgc = $this->entrybgc; diff --git a/lang/de/margic.php b/lang/de/margic.php index 7246529..6adbb11 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -37,7 +37,7 @@ $string['csvexport'] = 'Exportieren nach .csv'; $string['dateformat'] = 'Standard-Datumsformat'; $string['deadline'] = 'Offene Tage'; -$string['details'] = 'Details'; +$string['details'] = 'Statistik'; $string['margic:addentries'] = 'Margic-Einträge hinzufügen'; $string['margic:addinstance'] = 'Margic-Instanzen hinzufügen'; $string['margic:manageentries'] = 'Margic-Einträge verwalten'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 97ce28b..6775a6a 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -49,7 +49,7 @@ $string['csvexport'] = 'Export to .csv'; $string['deadline'] = 'Days Open'; $string['dateformat'] = 'Default date format'; -$string['details'] = 'Details'; +$string['details'] = 'Statistics'; $string['margicclosetime'] = 'Close time'; $string['margicclosetime_help'] = 'If this option is activated, you can set a date on which the Margic is closed. Participants will no longer be able to create or edit entries after that date.'; $string['margicentrydate'] = 'Set date for this entry'; diff --git a/lib.php b/lib.php index ae8a704..3c6fd61 100644 --- a/lib.php +++ b/lib.php @@ -607,7 +607,6 @@ function margic_reset_userdata($data) { /** * Removes all grades in the margic gradebook * - * @global object * @param int $courseid */ function margic_reset_gradebook($courseid) { diff --git a/mod_form.php b/mod_form.php index 09a1f58..fc3d3bd 100644 --- a/mod_form.php +++ b/mod_form.php @@ -125,6 +125,13 @@ public function definition() { $this->add_action_buttons(); } + /** + * Validate form. + * + * @param object $data The data from the form. + * @param object $files The files from the form. + * @return object $errors The errors. + */ public function validation($data, $files) { $errors = parent::validation($data, $files); diff --git a/styles.css b/styles.css index 1fea61d..043f1eb 100644 --- a/styles.css +++ b/styles.css @@ -218,13 +218,31 @@ } @media print { - .actionbuttons, - .activity-navigation, - #page-footer { + .path-mod-margic .actionbuttons, + .path-mod-margic .activity-navigation, + .path-mod-margic #page-footer { display: none; } - #page { + .path-mod-margic #page { margin-top: 0px !important; } + + .path-mod-margic .entry, + .path-mod-margic .entriesheader, + .path-mod-margic .entryfooter, + .path-mod-margic .annotationsheader, + .path-mod-margic .annotationarea { + background-color: #f2f2f2 !important; + } + + .path-mod-margic .entry .entry, + .path-mod-margic .annotation-box { + background-color: white !important; + } + + .path-mod-margic .needsedit, + .path-mod-margic .editend { + color: black; + } } From 0381ae5dee0a3fd22e4aaf3bb7091f25259e69d2 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 3 Aug 2022 17:54:04 +0200 Subject: [PATCH 30/60] feat (annotations): now fetching annotations via ajax --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 284 ++++++++++++++++--------------- annotations.php | 9 + locallib.php | 2 - view.php | 4 +- 6 files changed, 161 insertions(+), 142 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 5c238e4..d469e1b 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,3 +1,3 @@ -define("mod_margic/annotations",["exports","jquery"],(function(_exports,_jquery){var obj;function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}function removeAllTempHighlights(){var highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;i=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click","#id_cancel",(function(e){e.preventDefault(),removeAllTempHighlights(),resetForms(),edited=!1})),(0,_jquery.default)("textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){var selectedrange=window.getSelection().getRangeAt(0);if(""!==selectedrange.cloneContents().textContent&&canmakeannotations){removeAllTempHighlights(),resetForms();var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(xpathFromNode(selectedrange.startContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(xpathFromNode(selectedrange.endContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startposition"]').val(selectedrange.startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endposition"]').val(selectedrange.endOffset),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1);var annotatedtext=highlightRange(selectedrange,!1,"annotated_temp");""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotatedtext),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+entry+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1},success:function(response){annotations=JSON.parse(response),function(){for(var _i=0,_Object$values=Object.values(annotations);_i<_Object$values.length;_i++){var annotation=_Object$values[_i],newrange=document.createRange();try{newrange.setStart(nodeFromXPath(annotation.startcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.startposition),newrange.setEnd(nodeFromXPath(annotation.endcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.endposition)}catch(e){}var annotatedtext=highlightRange(newrange,annotation.id,"annotated",annotation.color);""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotatedtext)}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotationpreview-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).addClass("hovered"),(0,_jquery.default)(".annotation-box-"+id+" .errortype").addClass("hovered")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotationpreview-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).removeClass("hovered"),(0,_jquery.default)(".annotation-box-"+id+" .errortype").removeClass("hovered")})),(0,_jquery.default)(".annotatedtextpreview").mouseenter((function(){var id=this.id.replace("annotationpreview-","");(0,_jquery.default)(".annotated-"+id).addClass("hovered")})),(0,_jquery.default)(".annotatedtextpreview").mouseleave((function(){var id=this.id.replace("annotationpreview-","");(0,_jquery.default)(".annotated-"+id).removeClass("hovered")})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))}))},error:function(){alert("Error fetiching annotations")}})}})); //# sourceMappingURL=annotations.min.js.map \ No newline at end of file diff --git a/amd/build/annotations.min.js.map b/amd/build/annotations.min.js.map index 21bf62e..13a6099 100644 --- a/amd/build/annotations.min.js.map +++ b/amd/build/annotations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (annotations, canmakeannotations, myuserid) => {\n var edited = false;\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n\n // onclick listener if form is canceled\n $(document).on('click', '#id_cancel', function (e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n};"],"names":["annotations","canmakeannotations","myuserid","edited","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","length","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","textContent","parentNode","replaceChild","appendChild","_node$nodeValue","childNodes","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replaceWith","replacements","parent","r","remove","highlights","Array","from","querySelectorAll","undefined","pn","normalize","removeHighlights","removeClass","on","selectedrange","window","getSelection","getRangeAt","cloneContents","this","annotatedtext","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","preventDefault","keypress","which","parents","submit"],"mappings":"0+CAyBoB,SAACA,YAAaC,mBAAoBC,cAC9CC,QAAS,WA0CJC,eAAeC,iBAEhBF,QAAUE,aACVC,0BACAC,aACAJ,QAAS,OACN,GAAIF,oBAAsBC,UAAYF,YAAYK,cAAcG,OAAQ,CAC3EF,0BACAC,aAEAJ,OAASE,iBAELI,MAAQT,YAAYK,cAAcI,0BAEpC,mBAAqBJ,cAAcK,2BAEnC,oBAAsBD,MAAQ,iCAAiCE,IAAIX,YAAYK,cAAcO,oCAC7F,oBAAsBH,MAAQ,+BAA+BE,IAAIX,YAAYK,cAAcQ,kCAC3F,oBAAsBJ,MAAQ,gCAAgCE,IAAIX,YAAYK,cAAcS,mCAC5F,oBAAsBL,MAAQ,8BAA8BE,IAAIX,YAAYK,cAAcU,iCAE1F,oBAAsBN,MAAQ,+BAA+BE,IAAIN,kCAEjE,oBAAsBI,MAAQ,0BAA0BE,IAAIX,YAAYK,cAAcW,0BAEtF,oBAAsBP,MAAQ,WAAWE,IAAIX,YAAYK,cAAcY,0BAEvE,2BAA6BR,OAAOS,MAAK,mBAAE,sBAAwBb,cAAca,4BACjF,2BAA6BT,OAAOU,IAAI,eAAgB,IAAMnB,YAAYK,cAAce,2BAExF,mBAAqBX,MAAQ,qBAAqBY,aAAa,mBAAqBhB,kCACpF,mBAAqBI,MAAQ,qBAAqBa,2BAClD,mBAAqBb,MAAQ,aAAac,gCAE1C,mBAAqBlB,cAAckB,iBAOpChB,iCACH,oBAAoBG,2BAEpB,gDAAgDC,IAAI,0BAEpD,kDAAkDA,KAAK,uBACvD,gDAAgDA,KAAK,uBACrD,iDAAiDA,KAAK,uBACtD,+CAA+CA,KAAK,uBAEpD,2CAA2CA,IAAI,wBAE/C,mBAAmBa,IAAI,oBAAoBF,gBAYxCG,sBAAsBC,UACvBA,MAAMC,gBAIC,OAIPC,KAAOF,MAAMG,2BACbD,KAAKE,WAAaC,KAAKC,eAMvBJ,KAAOA,KAAKK,gBAEXL,WAGM,WAUPM,KAPEC,UAAY,GACZC,SACFR,KAAKS,cACPC,mBACEV,KACAW,WAAWC,WAGPN,KAAOE,SAASK,eACfC,cAAchB,MAAOQ,WAGtBlB,KAA4BkB,KAE5BlB,OAASU,MAAMiB,gBAAkBjB,MAAMkB,YAAc,EAGrD5B,KAAK6B,UAAUnB,MAAMkB,cAIrB5B,OAASU,MAAMoB,cAAgBpB,MAAMqB,UAAY/B,KAAKgC,KAAKC,QAE3DjC,KAAK6B,UAAUnB,MAAMqB,WAGzBZ,UAAUe,KAAKlC,cAGZmB,mBAaFgB,eAAezB,WAAOrB,qEAAsB+C,gEAAW,YAAahC,6DAAQ,SAE3Ee,UAAYV,sBAAsBC,OAIpC2B,cAAgB,GAChBC,SAAW,KACXC,YAAc,KAElBpB,UAAUqB,SAAQ,SAAAtB,MACVoB,UAAYA,SAASG,cAAgBvB,KACrCqB,YAAYL,KAAKhB,OAEjBqB,YAAc,CAACrB,MACfmB,cAAcH,KAAKK,cAEvBD,SAAWpB,YAMTwB,WAAa,QACnBL,cAAgBA,cAAcM,QAAO,SAAAC,aAEjCA,KAAKC,MAAK,SAAA3B,aAASwB,WAAWI,KAAK5B,KAAK6B,qBAIxCC,gBAAkB,UAEtBX,cAAcG,SAAQ,SAAAS,WACZC,YAAcC,SAASC,cAAc,QAC3CF,YAAYG,UAAYjB,SAEpB/C,eACA6D,YAAYG,WAAa,IAAMjB,SAAW,IAAM/C,aAEhD6D,YAAYI,MAAQ,sDAAwDlD,MAC5E8C,YAAYK,GAAKnB,SAAW,IAAM/C,aAClC6D,YAAYI,MAAME,gBAAkB,IAAMpD,OAG9C4C,iBAAmBC,MAAM,GAAGQ,YAE5BR,MAAM,GAAGS,WAAWC,aAAaT,YAAaD,MAAM,IACpDA,MAAMT,SAAQ,SAAAtB,aAAQgC,YAAYU,YAAY1C,YAI3C8B,yBAUFtB,cAAchB,MAAOQ,oDAEhBe,6DAASf,KAAK6B,4CAALc,gBAAgB5B,8DAAUf,KAAK4C,WAAW7B,cAGrDvB,MAAMqD,aAAa7C,KAAM,IAAM,GAE/BR,MAAMqD,aAAa7C,KAAMe,SAAW,EAE1C,MAAO+B,UAGE,YA4CNC,eAAe/C,UACdgD,cAnCWhD,UACXiD,SAAWjD,KAAKiD,SAASC,cAC3BC,OAASF,eACI,UAAbA,WACAE,OAAS,UAENA,OA6BMC,CAAYpD,MACnBqD,aArBerD,cACjBqD,IAAM,EAENC,IAAMtD,KACHsD,KACCA,IAAIL,WAAajD,KAAKiD,WACtBI,KAAO,GAEXC,IAAMA,IAAIC,uBAEPF,IAWKG,CAAgBxD,sBAClBgD,iBAAQK,kBAWbI,cAAczD,KAAMN,cACrBgE,MAAQ,GAGRC,KAAO3D,KACJ2D,OAASjE,MAAM,KACbiE,WACK,IAAIC,MAAM,oCAEpBF,MAAQX,eAAeY,MAAQ,IAAMD,MACrCC,KAAOA,KAAKnB,kBAGhBkB,OADAA,MAAQ,IAAMA,OACAG,QAAQ,MAAO,aAcxBC,eAAeC,QAASd,SAAUe,OACvCf,SAAWA,SAASgB,sBAEhBC,YAAc,EACTC,EAAI,EAAGA,EAAIJ,QAAQK,SAASrD,OAAQoD,IAAK,KACxCE,MAAQN,QAAQK,SAASD,MAC3BE,MAAMpB,SAASgB,gBAAkBhB,YAC/BiB,aACiBF,aACRK,aAKZ,cAwBFC,oBAAoBZ,MAAOhE,WAC2C,OAArDgE,MAAMa,MAAM,4CAExB,IAAIX,MAAM,wCAGdY,SAAWd,MAAMe,MAAM,KACzBV,QAAUrE,KAId8E,SAASE,uDAEWF,6DAAU,KAArBG,oBACDC,mBACAC,oBAEEC,aAAeH,QAAQI,QAAQ,SACf,IAAlBD,aAAqB,CACrBF,YAAcD,QAAQK,MAAM,EAAGF,kBAEzBG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,UACjEF,aAAeK,SAASD,UAAY,GACjB,SACR,UAGXL,YAAcD,QACdE,aAAe,MAGbR,MAAQP,eAAeC,QAASa,YAAaC,kBAC9CR,aACM,KAGXN,QAAUM,gEAGPN,iBAaFoB,cAAczB,WAAOhE,4DAAOuC,SAASmD,gBAE/Bd,oBAAoBZ,MAAOhE,MACpC,MAAO2F,YACEpD,SAASqD,SACZ,IAAM5B,MACNhE,KAIA,KACA6F,YAAYC,wBACZ,MACFC,0BAYDC,YAAY1F,KAAM2F,kBACjBC,OAA8B5F,KAAKwC,WAEzCmD,aAAarE,SAAQ,SAAAuE,UAAKD,OAAOzG,aAAa0G,EAAG7F,SACjDA,KAAK8F,kBAMA1H,8BACC2H,WAAaC,MAAMC,MAAK,mBAAE,QAAQ,GAAGC,iBAAiB,yBACzCC,IAAfJ,YAAiD,GAArBA,WAAWhF,iBAUrBgF,gBACjB,IAAI5B,EAAI,EAAGA,EAAI4B,WAAWhF,OAAQoD,OAC/B4B,WAAW5B,GAAG3B,WAAY,KACtB4D,GAAKL,WAAW5B,GAAG3B,WACjB4B,SAAW4B,MAAMC,KAAKF,WAAW5B,GAAGvB,YAC1C8C,YAAYK,WAAW5B,GAAIC,UAC3BgC,GAAGC,aAfPC,CAAiBP,gCAzcvB,oBAAoBvH,2BAGpB,iCAAiC+H,YAAY,gCAC7C,iCAAiCA,YAAY,gCAC7C,mCAAmCA,YAAY,kCAC/C,4BAA4BA,YAAY,2BAwdxCtE,UAAUuE,GAAG,UAAW,iBAAiB,eACnCC,cAAgBC,OAAOC,eAAeC,WAAW,MAEH,KAA9CH,cAAcI,gBAAgBtE,aAAsBxE,mBAAoB,CAExEK,0BAEAC,iBAEIE,MAAQuI,KAAKzE,GAAGwB,QAAQ,SAAU,wBAEpC,oBAAsBtF,MAAQ,iCAAiCE,IAC7DgF,cAAcgD,cAAchG,eAAgBqG,2BAC9C,oBAAsBvI,MAAQ,+BAA+BE,IAC3DgF,cAAcgD,cAAc7F,aAAckG,2BAC5C,oBAAsBvI,MAAQ,gCAAgCE,IAAIgI,cAAc/F,iCAChF,oBAAsBnC,MAAQ,8BAA8BE,IAAIgI,cAAc5F,+BAE9E,oBAAsBtC,MAAQ,WAAWE,IAAI,OAE3CsI,cAAgB9F,eAAewF,eAAe,EAAO,kBAEpC,IAAjBM,mCACE,2BAA6BxI,OAAOS,KAAK+H,mCAG7C,mBAAqBxI,MAAQ,qBAAqBa,2BAClD,oBAAsBb,MAAQ,aAAac,mDA5e1B2H,OAAOC,OAAOnJ,2CAAc,KAA1CoJ,8BAGDC,SAAWlF,SAASmF,kBAGpBD,SAASE,SACLlC,cAAc+B,WAAWxI,gBAAgB,mBAAE,UAAYwI,WAAW3I,OAAO,IAAK2I,WAAWtI,eAC7FuI,SAASG,OACLnC,cAAc+B,WAAWvI,cAAc,mBAAE,UAAYuI,WAAW3I,OAAO,IAAK2I,WAAWrI,aAC7F,MAAOiE,QAILiE,cAAgB9F,eAAekG,SAAUD,WAAW7E,GAAI,YAAa6E,WAAWhI,OAE/D,IAAjB6H,mCACE,sBAAwBG,WAAW7E,IAAIrD,KAAK+H,gBA+d1DQ,uBAGE,cAAcC,YAAW,eACnBnF,GAAKyE,KAAKzE,GAAGwB,QAAQ,aAAc,wBACrC,sBAAwBxB,IAAIoF,SAAS,+BACrC,cAAgBpF,IAAIoF,SAAS,+BAC7B,mBAAqBpF,GAAK,eAAeoF,SAAS,kCAItD,cAAcC,YAAW,eACnBrF,GAAKyE,KAAKzE,GAAGwB,QAAQ,aAAc,wBACrC,sBAAwBxB,IAAIkE,YAAY,+BACxC,cAAgBlE,IAAIkE,YAAY,+BAChC,mBAAqBlE,GAAK,eAAekE,YAAY,kCAIzD,yBAAyBiB,YAAW,eAC9BnF,GAAKyE,KAAKzE,GAAGwB,QAAQ,qBAAsB,wBAC7C,cAAgBxB,IAAIoF,SAAS,kCAGjC,yBAAyBC,YAAW,eAC9BrF,GAAKyE,KAAKzE,GAAGwB,QAAQ,qBAAsB,wBAC7C,cAAgBxB,IAAIkE,YAAY,kCAIpCtE,UAAUuE,GAAG,YAAa,mBAAmB,+BACzC,mBAAmBiB,SAAS,kCAGhCxF,UAAUuE,GAAG,aAAc,mBAAmB,+BAC1C,mBAAmBD,YAAY,kCAInCtE,UAAUuE,GAAG,QAAS,cAAc,WAElCtI,eADS4I,KAAKzE,GAAGwB,QAAQ,aAAc,4BAKzC5B,UAAUuE,GAAG,QAAS,oBAAoB,WAExCtI,eADS4I,KAAKzE,GAAGwB,QAAQ,mBAAoB,4BAW/C5B,UAAUuE,GAAG,QAAS,cAAc,SAAU1D,GAC5CA,EAAE6E,iBAEFvJ,0BAEAC,aAEAJ,QAAS,yBAIX,YAAY2J,UAAS,SAAU9E,GACd,IAAXA,EAAE+E,4BACAf,MAAMgB,QAAQ,UAAUC,SAC1BjF,EAAE6E"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1},\n success: function(response) {\n annotations = JSON.parse(response);\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n },\n error: function() {\n alert ('Error fetiching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n};"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","highlights","from","querySelectorAll","undefined","length","i","parentNode","pn","children","childNodes","replaceWith","normalize","removeHighlights","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","textContent","replaceChild","appendChild","_node$nodeValue","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replacements","parent","r","remove","removeClass","on","preventDefault","keypress","which","this","parents","submit","selectedrange","window","getSelection","getRangeAt","cloneContents","annotatedtext","ajax","url","success","response","JSON","parse","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","error","alert"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 0a29954..7db3cf5 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -23,8 +23,11 @@ import $ from 'jquery'; -export const init = (annotations, canmakeannotations, myuserid) => { +export const init = (cmid, canmakeannotations, myuserid) => { + var edited = false; + var annotations = Array(); + // Hide all Moodle forms. $('.annotation-form').hide(); @@ -34,11 +37,130 @@ export const init = (annotations, canmakeannotations, myuserid) => { $('.annotation-form div.form-group').removeClass('form-group'); $('.annotation-form div.row').removeClass('row'); + // Onclick listener if form is canceled. + $(document).on('click', '#id_cancel', function(e) { + e.preventDefault(); + + removeAllTempHighlights(); // Remove other temporary highlights. + + resetForms(); // Remove old form contents. + + edited = false; + }); + + // Listen for return key pressed to submit annotation form. + $('textarea').keypress(function (e) { + if (e.which == 13) { + $(this).parents(':eq(2)').submit(); + e.preventDefault(); + } + }); + + // If user selects text for new annotation + $(document).on('mouseup', '.originaltext', function () { + var selectedrange = window.getSelection().getRangeAt(0); + + if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) { + + removeAllTempHighlights(); // Remove other temporary highlights. + + resetForms(); // Remove old form contents. + + var entry = this.id.replace(/entry-/, ''); + + $('.annotation-form-' + entry + ' input[name="startcontainer"]').val( + xpathFromNode(selectedrange.startContainer, this)); + $('.annotation-form-' + entry + ' input[name="endcontainer"]').val( + xpathFromNode(selectedrange.endContainer, this)); + $('.annotation-form-' + entry + ' input[name="startposition"]').val(selectedrange.startOffset); + $('.annotation-form-' + entry + ' input[name="endposition"]').val(selectedrange.endOffset); + + $('.annotation-form-' + entry + ' select').val(1); + + var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp'); + + if (annotatedtext != '') { + $('#annotationpreview-temp-' + entry).html(annotatedtext); + } + + $('.annotationarea-' + entry + ' .annotation-form').show(); + $('.annotation-form-' + entry + ' #id_text').focus(); + } + }); + + // Fetch and recreate annotations. + $.ajax({ + url: './annotations.php', + data: {'id': cmid, 'getannotations': 1}, + success: function(response) { + annotations = JSON.parse(response); + recreateAnnotations(); + + // Highlight annotation and all annotated text if annotated text is hovered + $('.annotated').mouseenter(function () { + var id = this.id.replace('annotated-', ''); + $('.annotationpreview-' + id).addClass('hovered'); + $('.annotated-' + id).addClass('hovered'); + $('.annotation-box-' + id + ' .errortype').addClass('hovered'); + + }); + + $('.annotated').mouseleave(function () { + var id = this.id.replace('annotated-', ''); + $('.annotationpreview-' + id).removeClass('hovered'); + $('.annotated-' + id).removeClass('hovered'); + $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); + }); + + // Highlight annotated text if annotationpreview is hovered + $('.annotatedtextpreview').mouseenter(function () { + var id = this.id.replace('annotationpreview-', ''); + $('.annotated-' + id).addClass('hovered'); + }); + + $('.annotatedtextpreview').mouseleave(function () { + var id = this.id.replace('annotationpreview-', ''); + $('.annotated-' + id).removeClass('hovered'); + }); + + // Highlight whole temp annotation if part of temp annotation is hovered + $(document).on('mouseover', '.annotated_temp', function () { + $('.annotated_temp').addClass('hovered'); + }); + + $(document).on('mouseleave', '.annotated_temp', function () { + $('.annotated_temp').removeClass('hovered'); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.annotated', function () { + var id = this.id.replace('annotated-', ''); + editAnnotation(id); + }); + + // Onclick listener for editing annotation. + $(document).on('click', '.edit-annotation', function () { + var id = this.id.replace('edit-annotation-', ''); + editAnnotation(id); + }); + + // Onclick listener for click on annotation-box. + // $(document).on('click', '.annotation-box', function() { + // var id = this.id.replace('annotation-box-', ''); + // $('#annotated-' + id).focus(); + // }); + }, + error: function() { + alert ('Error fetiching annotations'); + } + }); + /** * Recreate annotations. * */ function recreateAnnotations() { + for (let annotation of Object.values(annotations)) { // Recreate range from db. @@ -122,6 +244,32 @@ export const init = (annotations, canmakeannotations, myuserid) => { $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation. } + /** + * Remove all temporary highlights under a given root element. + */ + function removeAllTempHighlights() { + const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); + if (highlights !== undefined && highlights.length != 0) { + removeHighlights(highlights); + } + } + + /** + * Remove highlights from a range previously highlighted with `highlightRange`. + * + * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange` + */ + function removeHighlights(highlights) { + for (var i = 0; i < highlights.length; i++) { + if (highlights[i].parentNode) { + var pn = highlights[i].parentNode; + const children = Array.from(highlights[i].childNodes); + replaceWith(highlights[i], children); + pn.normalize(); + } + } + } + /** * Return text nodes which are entirely inside `range`. * @@ -476,138 +624,4 @@ export const init = (annotations, canmakeannotations, myuserid) => { replacements.forEach(r => parent.insertBefore(r, node)); node.remove(); } - - /** - * Remove all temporary highlights under a given root element. - */ - function removeAllTempHighlights() { - const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp')); - if (highlights !== undefined && highlights.length != 0) { - removeHighlights(highlights); - } - } - - /** - * Remove highlights from a range previously highlighted with `highlightRange`. - * - * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange` - */ - function removeHighlights(highlights) { - for (var i = 0; i < highlights.length; i++) { - if (highlights[i].parentNode) { - var pn = highlights[i].parentNode; - const children = Array.from(highlights[i].childNodes); - replaceWith(highlights[i], children); - pn.normalize(); - } - } - } - - // If user selects text for new annotation - $(document).on('mouseup', '.originaltext', function () { - var selectedrange = window.getSelection().getRangeAt(0); - - if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) { - - removeAllTempHighlights(); // Remove other temporary highlights. - - resetForms(); // Remove old form contents. - - var entry = this.id.replace(/entry-/, ''); - - $('.annotation-form-' + entry + ' input[name="startcontainer"]').val( - xpathFromNode(selectedrange.startContainer, this)); - $('.annotation-form-' + entry + ' input[name="endcontainer"]').val( - xpathFromNode(selectedrange.endContainer, this)); - $('.annotation-form-' + entry + ' input[name="startposition"]').val(selectedrange.startOffset); - $('.annotation-form-' + entry + ' input[name="endposition"]').val(selectedrange.endOffset); - - $('.annotation-form-' + entry + ' select').val(1); - - var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp'); - - if (annotatedtext != '') { - $('#annotationpreview-temp-' + entry).html(annotatedtext); - } - - $('.annotationarea-' + entry + ' .annotation-form').show(); - $('.annotation-form-' + entry + ' #id_text').focus(); - } - }); - - recreateAnnotations(); - - // Highlight annotation and all annotated text if annotated text is hovered - $('.annotated').mouseenter(function () { - var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).addClass('hovered'); - $('.annotated-' + id).addClass('hovered'); - $('.annotation-box-' + id + ' .errortype').addClass('hovered'); - - }); - - $('.annotated').mouseleave(function () { - var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).removeClass('hovered'); - $('.annotated-' + id).removeClass('hovered'); - $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); - }); - - // Highlight annotated text if annotationpreview is hovered - $('.annotatedtextpreview').mouseenter(function () { - var id = this.id.replace('annotationpreview-', ''); - $('.annotated-' + id).addClass('hovered'); - }); - - $('.annotatedtextpreview').mouseleave(function () { - var id = this.id.replace('annotationpreview-', ''); - $('.annotated-' + id).removeClass('hovered'); - }); - - // Highlight whole temp annotation if part of temp annotation is hovered - $(document).on('mouseover', '.annotated_temp', function () { - $('.annotated_temp').addClass('hovered'); - }); - - $(document).on('mouseleave', '.annotated_temp', function () { - $('.annotated_temp').removeClass('hovered'); - }); - - // Onclick listener for editing annotation. - $(document).on('click', '.annotated', function () { - var id = this.id.replace('annotated-', ''); - editAnnotation(id); - }); - - // Onclick listener for editing annotation. - $(document).on('click', '.edit-annotation', function () { - var id = this.id.replace('edit-annotation-', ''); - editAnnotation(id); - }); - - // Onclick listener for click on annotation-box. - // $(document).on('click', '.annotation-box', function() { - // var id = this.id.replace('annotation-box-', ''); - // $('#annotated-' + id).focus(); - // }); - - // onclick listener if form is canceled - $(document).on('click', '#id_cancel', function (e) { - e.preventDefault(); - - removeAllTempHighlights(); // Remove other temporary highlights. - - resetForms(); // Remove old form contents. - - edited = false; - }); - - // Listen for return key pressed to submit annotation form. - $('textarea').keypress(function (e) { - if (e.which == 13) { - $(this).parents(':eq(2)').submit(); - e.preventDefault(); - } - }); - }; \ No newline at end of file diff --git a/annotations.php b/annotations.php index 3952d69..a8146ee 100644 --- a/annotations.php +++ b/annotations.php @@ -35,6 +35,9 @@ // Module instance ID as alternative. $m = optional_param('m', null, PARAM_INT); +// Param if annotations should be returned via ajax. +$getannotations = optional_param('getannotations', 0, PARAM_INT); + // Param if annotation should be deleted. $deleteannotation = optional_param('deleteannotation', 0, PARAM_INT); // Annotation to be deleted. @@ -75,6 +78,12 @@ $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); +// Get annotation (ajax). +if ($getannotations) { + echo json_encode($margic->get_annotations()); + die; +} + // Delete annotation. if (has_capability('mod/margic:makeannotations', $context) && $deleteannotation !== 0) { global $USER; diff --git a/locallib.php b/locallib.php index aac7500..7401375 100644 --- a/locallib.php +++ b/locallib.php @@ -321,8 +321,6 @@ public function get_entries() { * @return array action */ public function get_annotations() { - global $DB, $USER; - return $this->annotations; } diff --git a/view.php b/view.php index 37b8605..c4dc4b1 100644 --- a/view.php +++ b/view.php @@ -125,9 +125,7 @@ $PAGE->navbar->add(get_string('viewannotations', 'mod_margic')); $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', - array('annotations' => $margic->get_annotations(), - 'canmakeannotations' => $canmakeannotations, - 'myuserid' => $USER->id)); + array( 'cmid' => $cm->id, 'canmakeannotations' => $canmakeannotations, 'myuserid' => $USER->id)); } else { // Header. $PAGE->set_url('/mod/margic/view.php', array( From fe1da416e0aeb583115440ddab67a73c45f9db38 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Wed, 3 Aug 2022 19:03:10 +0200 Subject: [PATCH 31/60] fix (annotations): small fix for fetching annotations via ajax and some language string changes --- annotations.php | 12 ++++++------ lang/de/margic.php | 27 +++++++++++++-------------- lang/en/margic.php | 7 +++---- locallib.php | 2 +- templates/margic_view.mustache | 10 +++++++--- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/annotations.php b/annotations.php index a8146ee..03ce345 100644 --- a/annotations.php +++ b/annotations.php @@ -68,6 +68,12 @@ require_login($course, true, $cm); +// Get annotation (ajax). +if ($getannotations) { + echo json_encode($margic->get_annotations()); + die; +} + require_capability('mod/margic:makeannotations', $context); // Header. @@ -78,12 +84,6 @@ $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); -// Get annotation (ajax). -if ($getannotations) { - echo json_encode($margic->get_annotations()); - die; -} - // Delete annotation. if (has_capability('mod/margic:makeannotations', $context) && $deleteannotation !== 0) { global $USER; diff --git a/lang/de/margic.php b/lang/de/margic.php index 6adbb11..c9a10fe 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -132,28 +132,27 @@ $string['usertoolbar'] = 'Werkzeuge:'; $string['viewallentries'] = 'Anzeigen von {$a} Margic-Einträgen'; -$string['startnewentry'] = 'Neuen Eintrag schreiben'; +$string['startnewentry'] = 'Neuer Eintrag'; $string['viewentries'] = 'Einträge ansehen'; $string['numwordsraw'] = '{$a->wordscount} Wörter mit {$a->charscount} Zeichen, einschließlich {$a->spacescount} Leerzeichen. '; $string['margicentrydate'] = 'Datum des Eintrages bestimmen'; -$string['margic:viewannotations'] = 'Annotierungen ansehen'; -$string['margic:makeannotations'] = 'Annotierungen anlegen'; -$string['annotations'] = 'Annotierungen'; -$string['viewannotations'] = 'Annotierungen ansehen'; -$string['viewandmakeannotations'] = 'Annotierungen erstellen und ansehen'; -$string['hideannotations'] = 'Annotierungen verstecken'; -$string['annotationadded'] = 'Annotierung hinzugefügt'; -$string['annotationedited'] = 'Annotierung geändert'; -$string['annotationdeleted'] = 'Annotierung gelöscht'; -$string['annotationinvalid'] = 'Annotierung ungültig'; +$string['margic:viewannotations'] = 'Annotationen ansehen'; +$string['margic:makeannotations'] = 'Annotationen anlegen'; +$string['annotations'] = 'Annotationen'; +$string['viewannotations'] = 'Annotationen ansehen'; +$string['hideannotations'] = 'Annotationen verstecken'; +$string['annotationadded'] = 'Annotation hinzugefügt'; +$string['annotationedited'] = 'Annotation geändert'; +$string['annotationdeleted'] = 'Annotation gelöscht'; +$string['annotationinvalid'] = 'Annotation ungültig'; $string['noentriesfound'] = 'Keine Einträge gefunden'; $string['lastedited'] = 'Zuletzt bearbeitet'; $string['getallentriesofuser'] = 'Alle Margic Enträge dieses Benutzers anzeigen'; $string['myentries'] = 'Meine Einträge'; $string['forallentries'] = 'für alle Einträge von'; $string['forallmyentries'] = 'für alle meine Einträge'; -$string['toggleratingform'] = 'Bewertungsmodus öffnen/schließen'; +$string['toggleratingform'] = 'Bewerten'; $string['norating'] = 'Bewertung deaktiviert.'; $string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; $string['startoreditentry'] = 'Eintrag anlegen oder bearbeiten'; @@ -181,7 +180,7 @@ $string['punctuation'] = 'Interpunktion'; $string['other'] = 'Sonstiges'; -$string['annotationssummary'] = 'Annotationsauswertung und Fehlertypen'; +$string['annotationssummary'] = 'Fehlerauswertung'; $string['participant'] = 'TeilnehmerIn'; $string['backtooverview'] = 'Zurück zur Übersicht'; $string['adderrortype'] = 'Fehlertyp anlegen'; @@ -214,7 +213,7 @@ $string['explanationhexcolor_help'] = 'Die Farbe des Fehlertypen als Hexadezimalwert. Dieser besteht aus genau 6 Zeichen (A-F sowie 0-9) und repräsentiert eine Farbe. Den Hexwert von beliebigen Farben kann man z. B. unter https://www.w3schools.com/colors/colors_picker.asp herausfinden.'; $string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Fehlertyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre Margics ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren Margics verwendet werden.'; $string['annotatedtextnotfound'] = 'Annotierter Text nicht gefunden'; -$string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist (z. B. durch eine nachträgliche Änderung des ursprünglichen Beitrags) ungültig geworden. Die Markierung für diese Annotierung muss deshalb neu gesetzt werden.'; +$string['annotatedtextinvalid'] = 'Der ursprünglich annotierte Text ist (z. B. durch eine nachträgliche Änderung des ursprünglichen Beitrags) ungültig geworden. Die Markierung für diese Annotation muss deshalb neu gesetzt werden.'; $string['notallowedtodothis'] = 'Vorgang nicht möglich.'; $string['deletederrortype'] = 'Gelöschter Typ'; $string['errtypedeleted'] = 'Fehlertyp nicht vorhanden.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 6775a6a..bb62bad 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -137,7 +137,7 @@ $string['sorthighestentry'] = 'From highest rated margic entry to the lowest rated entry.'; $string['sortlastentry'] = 'From latest modified margic entry to the oldest modified entry.'; $string['sortoptions'] = ' Sort options: '; -$string['startnewentry'] = 'Start new entry'; +$string['startnewentry'] = 'New entry'; $string['teacher'] = 'Teacher'; $string['text'] = 'Text'; $string['timecreated'] = 'Time created'; @@ -153,7 +153,6 @@ $string['margic:makeannotations'] = 'Make annotations'; $string['annotations'] = 'Annotations'; $string['viewannotations'] = 'View annotations'; -$string['viewandmakeannotations'] = 'View and create annotations'; $string['hideannotations'] = 'Hide annotations'; $string['annotationadded'] = 'Annotation added'; $string['annotationedited'] = 'Annotation edited'; @@ -166,7 +165,7 @@ $string['numwordsraw'] = '{$a->wordscount} text words using {$a->charscount} characters, including {$a->spacescount} spaces. '; $string['forallentries'] = 'for all entries of'; $string['forallmyentries'] = 'for all of my entries'; -$string['toggleratingform'] = 'Open/close rating form'; +$string['toggleratingform'] = 'Grading'; $string['norating'] = 'Rating disabled.'; $string['viewallmargics'] = 'View all margics in course'; $string['startoreditentry'] = 'Add or edit entry'; @@ -194,7 +193,7 @@ $string['punctuation'] = 'Punctuation'; $string['other'] = 'Other'; -$string['annotationssummary'] = 'Annotations summary and error types'; +$string['annotationssummary'] = 'Error summary'; $string['participant'] = 'Participant'; $string['backtooverview'] = 'Back to overview'; $string['adderrortype'] = 'Add error type'; diff --git a/locallib.php b/locallib.php index 7401375..54b3919 100644 --- a/locallib.php +++ b/locallib.php @@ -604,7 +604,7 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode $position = 0; $doc = new DOMDocument(); - $doc->loadHTML($entry->text); + @$doc->loadHTML($entry->text); $this->index_original($doc); diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 889a6d7..7bd9efd 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -47,12 +47,11 @@ {{#canmanageentries}}{{/canmanageentries}} {{#canmanageentries}}{{#singleuser}} {{#str}}viewallentries, mod_margic{{/str}} {{/singleuser}}{{/canmanageentries}} {{^annotationmode}} - {{#canmakeannotations}} {{#str}}viewandmakeannotations, mod_margic{{/str}} {{/canmakeannotations}} + {{#canmakeannotations}} {{#str}}annotations, mod_margic{{/str}} {{/canmakeannotations}} {{^canmakeannotations}} {{#str}}viewannotations, mod_margic{{/str}} {{/canmakeannotations}} {{/annotationmode}} {{#annotationmode}} {{#str}}hideannotations, mod_margic{{/str}} - {{/annotationmode}} {{/entries.0}} {{#canmanageentries}} {{#str}}annotationssummary, mod_margic{{/str}} {{/canmanageentries}} @@ -101,7 +100,12 @@
{{#annotationmode}}
-

{{#str}} annotations, mod_margic {{/str}}

+

+ {{#str}} annotations, mod_margic {{/str}} + {{#annotationmode}} + + {{/annotationmode}} +

{{/annotationmode}}
From 7e37071768d7f377411ec5da8cb8fedf6629da39 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 4 Aug 2022 10:29:30 +0200 Subject: [PATCH 32/60] feat (annotations): small js improvements --- amd/build/annotations.min.js | 2 +- amd/build/annotations.min.js.map | 2 +- amd/src/annotations.js | 47 +++++++++++++------------------- lang/de/margic.php | 1 + lang/en/margic.php | 1 + styles.css | 5 ---- templates/margic_entry.mustache | 1 + 7 files changed, 24 insertions(+), 35 deletions(-) diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index d469e1b..9103b2e 100644 --- a/amd/build/annotations.min.js +++ b/amd/build/annotations.min.js @@ -1,3 +1,3 @@ -define("mod_margic/annotations",["exports","jquery"],(function(_exports,_jquery){var obj;function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click","#id_cancel",(function(e){e.preventDefault(),removeAllTempHighlights(),resetForms(),edited=!1})),(0,_jquery.default)("textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){var selectedrange=window.getSelection().getRangeAt(0);if(""!==selectedrange.cloneContents().textContent&&canmakeannotations){removeAllTempHighlights(),resetForms();var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(xpathFromNode(selectedrange.startContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(xpathFromNode(selectedrange.endContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startposition"]').val(selectedrange.startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endposition"]').val(selectedrange.endOffset),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1);var annotatedtext=highlightRange(selectedrange,!1,"annotated_temp");""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotatedtext),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+entry+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1},success:function(response){annotations=JSON.parse(response),function(){for(var _i=0,_Object$values=Object.values(annotations);_i<_Object$values.length;_i++){var annotation=_Object$values[_i],newrange=document.createRange();try{newrange.setStart(nodeFromXPath(annotation.startcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.startposition),newrange.setEnd(nodeFromXPath(annotation.endcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.endposition)}catch(e){}var annotatedtext=highlightRange(newrange,annotation.id,"annotated",annotation.color);""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotatedtext)}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotationpreview-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).addClass("hovered"),(0,_jquery.default)(".annotation-box-"+id+" .errortype").addClass("hovered")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotationpreview-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).removeClass("hovered"),(0,_jquery.default)(".annotation-box-"+id+" .errortype").removeClass("hovered")})),(0,_jquery.default)(".annotatedtextpreview").mouseenter((function(){var id=this.id.replace("annotationpreview-","");(0,_jquery.default)(".annotated-"+id).addClass("hovered")})),(0,_jquery.default)(".annotatedtextpreview").mouseleave((function(){var id=this.id.replace("annotationpreview-","");(0,_jquery.default)(".annotated-"+id).removeClass("hovered")})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))}))},error:function(){alert("Error fetiching annotations")}})}})); +define("mod_margic/annotations",["exports","jquery"],(function(_exports,_jquery){var obj;function _createForOfIteratorHelper(o,allowArrayLike){var it="undefined"!=typeof Symbol&&o[Symbol.iterator]||o["@@iterator"];if(!it){if(Array.isArray(o)||(it=function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(o))||allowArrayLike&&o&&"number"==typeof o.length){it&&(o=it);var i=0,F=function(){};return{s:F,n:function(){return i>=o.length?{done:!0}:{done:!1,value:o[i++]}},e:function(_e){throw _e},f:F}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var err,normalCompletion=!0,didErr=!1;return{s:function(){it=it.call(o)},n:function(){var step=it.next();return normalCompletion=step.done,step},e:function(_e2){didErr=!0,err=_e2},f:function(){try{normalCompletion||null==it.return||it.return()}finally{if(didErr)throw err}}}}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset1&&void 0!==arguments[1]&&arguments[1],cssClass=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"annotated",color=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"FFFF00",textNodes=wholeTextNodesInRange(range),textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((function(node){prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));var whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((function(span){return span.some((function(node){return!whitespace.test(node.nodeValue)}))}));var hihglightedtext="";return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("span");highlightEl.className=cssClass,annotationid&&(highlightEl.className+=" "+cssClass+"-"+annotationid,highlightEl.style="text-decoration:underline; text-decoration-color: #"+color,highlightEl.id=cssClass+"-"+annotationid,highlightEl.style.backgroundColor="#"+color),hihglightedtext+=nodes[0].textContent,nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)}))})),hihglightedtext}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue,length=null!==(_node$nodeValue$lengt=null===(_node$nodeValue=node.nodeValue)||void 0===_node$nodeValue?void 0:_node$nodeValue.length)&&void 0!==_node$nodeValue$lengt?_node$nodeValue$lengt:node.childNodes.length;return range.comparePoint(node,0)<=0&&range.comparePoint(node,length)>=0}catch(e){return!1}}function getPathSegment(node){var name=function(node){var nodeName=node.nodeName.toLowerCase(),result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){for(var pos=0,tmp=node;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function xpathFromNode(node,root){for(var xpath="",elem=node;elem!==root;){if(!elem)throw new Error("Node is not a descendant of root");xpath=getPathSegment(elem)+"/"+xpath,elem=elem.parentNode}return xpath=(xpath="/"+xpath).replace(/\/$/,"")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();for(var matchIndex=-1,i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return evaluateSimpleXPath(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)(".annotation-form div.col-md-3").removeClass("col-md-3"),(0,_jquery.default)(".annotation-form div.col-md-9").removeClass("col-md-9"),(0,_jquery.default)(".annotation-form div.form-group").removeClass("form-group"),(0,_jquery.default)(".annotation-form div.row").removeClass("row"),(0,_jquery.default)(document).on("click","#id_cancel",(function(e){e.preventDefault(),removeAllTempHighlights(),resetForms(),edited=!1})),(0,_jquery.default)("textarea").keypress((function(e){13==e.which&&((0,_jquery.default)(this).parents(":eq(2)").submit(),e.preventDefault())})),(0,_jquery.default)(document).on("mouseup",".originaltext",(function(){var selectedrange=window.getSelection().getRangeAt(0);if(""!==selectedrange.cloneContents().textContent&&canmakeannotations){removeAllTempHighlights(),resetForms();var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(xpathFromNode(selectedrange.startContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(xpathFromNode(selectedrange.endContainer,this)),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startposition"]').val(selectedrange.startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endposition"]').val(selectedrange.endOffset),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1);var annotatedtext=highlightRange(selectedrange,!1,"annotated_temp");""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotatedtext),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotation-form-"+entry+" #id_text").focus()}})),_jquery.default.ajax({url:"./annotations.php",data:{id:cmid,getannotations:1},success:function(response){annotations=JSON.parse(response),function(){for(var _i=0,_Object$values=Object.values(annotations);_i<_Object$values.length;_i++){var annotation=_Object$values[_i],newrange=document.createRange();try{newrange.setStart(nodeFromXPath(annotation.startcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.startposition),newrange.setEnd(nodeFromXPath(annotation.endcontainer,(0,_jquery.default)("#entry-"+annotation.entry)[0]),annotation.endposition)}catch(e){}var annotatedtext=highlightRange(newrange,annotation.id,"annotated",annotation.color);""!=annotatedtext&&(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotatedtext)}}(),(0,_jquery.default)(".annotated").mouseenter((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).addClass("hovered"),(0,_jquery.default)(".annotated-"+id).addClass("hovered")})),(0,_jquery.default)(".annotated").mouseleave((function(){var id=this.id.replace("annotated-","");(0,_jquery.default)(".annotation-box-"+id).removeClass("hovered"),(0,_jquery.default)(".annotated-"+id).removeClass("hovered")})),(0,_jquery.default)(document).on("mouseover",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".annotated_temp",(function(){(0,_jquery.default)(".annotated_temp").removeClass("hovered")})),(0,_jquery.default)(document).on("click",".annotated",(function(){editAnnotation(this.id.replace("annotated-",""))})),(0,_jquery.default)(document).on("click",".edit-annotation",(function(){editAnnotation(this.id.replace("edit-annotation-",""))})),(0,_jquery.default)(document).on("mouseover",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).addClass("hovered")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).removeClass("hovered")}))},error:function(){alert("Error fetiching annotations")}})}})); //# sourceMappingURL=annotations.min.js.map \ No newline at end of file diff --git a/amd/build/annotations.min.js.map b/amd/build/annotations.min.js.map index 13a6099..325afef 100644 --- a/amd/build/annotations.min.js.map +++ b/amd/build/annotations.min.js.map @@ -1 +1 @@ -{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1},\n success: function(response) {\n annotations = JSON.parse(response);\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n $('.annotation-box-' + id + ' .errortype').addClass('hovered');\n\n });\n\n $('.annotated').mouseleave(function () {\n var id = this.id.replace('annotated-', '');\n $('.annotationpreview-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n $('.annotation-box-' + id + ' .errortype').removeClass('hovered');\n });\n\n // Highlight annotated text if annotationpreview is hovered\n $('.annotatedtextpreview').mouseenter(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotatedtextpreview').mouseleave(function () {\n var id = this.id.replace('annotationpreview-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function () {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function () {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function () {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function () {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for click on annotation-box.\n // $(document).on('click', '.annotation-box', function() {\n // var id = this.id.replace('annotation-box-', '');\n // $('#annotated-' + id).focus();\n // });\n },\n error: function() {\n alert ('Error fetiching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n // highlightEl.tabIndex = 1;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n};"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","highlights","from","querySelectorAll","undefined","length","i","parentNode","pn","children","childNodes","replaceWith","normalize","removeHighlights","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","textContent","replaceChild","appendChild","_node$nodeValue","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replacements","parent","r","remove","removeClass","on","preventDefault","keypress","which","this","parents","submit","selectedrange","window","getSelection","getRangeAt","cloneContents","annotatedtext","ajax","url","success","response","JSON","parse","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","error","alert"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"annotations.min.js","sources":["../src/annotations.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 * Module for the annotation functions of the margic.\n *\n * @module mod_margic/annotations\n * @copyright 2022 coactum GmbH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n // Hide all Moodle forms.\n $('.annotation-form').hide();\n\n // Remove col-mds from moodle form.\n $('.annotation-form div.col-md-3').removeClass('col-md-3');\n $('.annotation-form div.col-md-9').removeClass('col-md-9');\n $('.annotation-form div.form-group').removeClass('form-group');\n $('.annotation-form div.row').removeClass('row');\n\n // Onclick listener if form is canceled.\n $(document).on('click', '#id_cancel', function(e) {\n e.preventDefault();\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n edited = false;\n });\n\n // Listen for return key pressed to submit annotation form.\n $('textarea').keypress(function (e) {\n if (e.which == 13) {\n $(this).parents(':eq(2)').submit();\n e.preventDefault();\n }\n });\n\n // If user selects text for new annotation\n $(document).on('mouseup', '.originaltext', function () {\n var selectedrange = window.getSelection().getRangeAt(0);\n\n if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Remove old form contents.\n\n var entry = this.id.replace(/entry-/, '');\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n xpathFromNode(selectedrange.startContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n xpathFromNode(selectedrange.endContainer, this));\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(selectedrange.startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(selectedrange.endOffset);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n var annotatedtext = highlightRange(selectedrange, false, 'annotated_temp');\n\n if (annotatedtext != '') {\n $('#annotationpreview-temp-' + entry).html(annotatedtext);\n }\n\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotation-form-' + entry + ' #id_text').focus();\n }\n });\n\n // Fetch and recreate annotations.\n $.ajax({\n url: './annotations.php',\n data: {'id': cmid, 'getannotations': 1},\n success: function(response) {\n annotations = JSON.parse(response);\n recreateAnnotations();\n\n // Highlight annotation and all annotated text if annotated text is hovered\n $('.annotated').mouseenter(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).addClass('hovered');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n // Highlight whole temp annotation if part of temp annotation is hovered\n $(document).on('mouseover', '.annotated_temp', function() {\n $('.annotated_temp').addClass('hovered');\n });\n\n $(document).on('mouseleave', '.annotated_temp', function() {\n $('.annotated_temp').removeClass('hovered');\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.annotated', function() {\n var id = this.id.replace('annotated-', '');\n editAnnotation(id);\n });\n\n // Onclick listener for editing annotation.\n $(document).on('click', '.edit-annotation', function() {\n var id = this.id.replace('edit-annotation-', '');\n editAnnotation(id);\n });\n\n // Highlight annotation if hoverannotation button is hovered\n $(document).on('mouseover', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).addClass('hovered');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).removeClass('hovered');\n });\n\n },\n error: function() {\n alert ('Error fetiching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n // Recreate range from db.\n var newrange = document.createRange();\n\n try {\n newrange.setStart(\n nodeFromXPath(annotation.startcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.startposition);\n newrange.setEnd(\n nodeFromXPath(annotation.endcontainer, $(\"#entry-\" + annotation.entry)[0]), annotation.endposition);\n } catch (e) {\n // eslint-disable-line\n }\n\n var annotatedtext = highlightRange(newrange, annotation.id, 'annotated', annotation.color);\n\n if (annotatedtext != '') {\n $('#annotationpreview-' + annotation.id).html(annotatedtext);\n }\n }\n }\n\n /**\n * Edit annotation.\n *\n * @param {int} annotationid\n */\n function editAnnotation(annotationid) {\n\n if (edited == annotationid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n edited = false;\n } else if (canmakeannotations && myuserid == annotations[annotationid].userid) {\n removeAllTempHighlights(); // Remove other temporary highlights.\n resetForms(); // Remove old form contents.\n\n edited = annotationid;\n\n var entry = annotations[annotationid].entry;\n\n $('.annotation-box-' + annotationid).hide(); // Hide edited annotation-box.\n\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(annotations[annotationid].startcontainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(annotations[annotationid].endcontainer);\n $('.annotation-form-' + entry + ' input[name=\"startposition\"]').val(annotations[annotationid].startposition);\n $('.annotation-form-' + entry + ' input[name=\"endposition\"]').val(annotations[annotationid].endposition);\n\n $('.annotation-form-' + entry + ' input[name=\"annotationid\"]').val(annotationid);\n\n $('.annotation-form-' + entry + ' textarea[name=\"text\"]').val(annotations[annotationid].text);\n\n $('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);\n\n $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());\n $('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);\n\n $('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);\n $('.annotationarea-' + entry + ' .annotation-form').show();\n $('.annotationarea-' + entry + ' #id_text').focus();\n } else {\n $('.annotation-box-' + annotationid).focus();\n }\n }\n\n /**\n * Reset all annotation forms\n */\n function resetForms() {\n $('.annotation-form').hide();\n\n $('.annotation-form input[name^=\"annotationid\"]').val(null);\n\n $('.annotation-form input[name^=\"startcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"endcontainer\"]').val(-1);\n $('.annotation-form input[name^=\"startposition\"]').val(-1);\n $('.annotation-form input[name^=\"endposition\"]').val(-1);\n\n $('.annotation-form textarea[name^=\"text\"]').val('');\n\n $('.annotation-box').not('.annotation-form').show(); // To show again edited annotation.\n }\n\n /**\n * Remove all temporary highlights under a given root element.\n */\n function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n }\n\n /**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n pn.normalize();\n }\n }\n }\n\n /**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n }\n\n /**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n var hihglightedtext = '';\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('span');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n hihglightedtext += nodes[0].textContent;\n\n nodes[0].parentNode.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n });\n\n return hihglightedtext;\n }\n\n /**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\n function isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n }\n\n /**\n * Get the node name for use in generating an xpath expression.\n *\n * @param {Node} node\n * @return {string} - Name of the node\n */\n function getNodeName(node) {\n const nodeName = node.nodeName.toLowerCase();\n let result = nodeName;\n if (nodeName === '#text') {\n result = 'text()';\n }\n return result;\n }\n\n /**\n * Get the index of the node as it appears in its parent's child list\n *\n * @param {Node} node\n * @return {int} - Position of the node\n */\n function getNodePosition(node) {\n let pos = 0;\n /** @type {Node|null} */\n let tmp = node;\n while (tmp) {\n if (tmp.nodeName === node.nodeName) {\n pos += 1;\n }\n tmp = tmp.previousSibling;\n }\n return pos;\n }\n\n /**\n * Get the path segments to the node\n *\n * @param {Node} node\n * @return {array} - Path segments\n */\n function getPathSegment(node) {\n const name = getNodeName(node);\n const pos = getNodePosition(node);\n return `${name}[${pos}]`;\n }\n\n /**\n * A simple XPath generator which can generate XPaths of the form\n * /tag[index]/tag[index].\n *\n * @param {Node} node - The node to generate a path to\n * @param {Node} root - Root node to which the returned path is relative\n * @return {string} - The xpath of a node\n */\n function xpathFromNode(node, root) {\n let xpath = '';\n\n /** @type {Node|null} */\n let elem = node;\n while (elem !== root) {\n if (!elem) {\n throw new Error('Node is not a descendant of root');\n }\n xpath = getPathSegment(elem) + '/' + xpath;\n elem = elem.parentNode;\n }\n xpath = '/' + xpath;\n xpath = xpath.replace(/\\/$/, ''); // Remove trailing slash\n\n return xpath;\n }\n\n /**\n * Return the `index`'th immediate child of `element` whose tag name is\n * `nodeName` (case insensitive).\n *\n * @param {Element} element\n * @param {string} nodeName\n * @param {number} index\n * @return {Element|null} - The child element or null\n */\n function nthChildOfType(element, nodeName, index) {\n nodeName = nodeName.toUpperCase();\n\n let matchIndex = -1;\n for (let i = 0; i < element.children.length; i++) {\n const child = element.children[i];\n if (child.nodeName.toUpperCase() === nodeName) {\n ++matchIndex;\n if (matchIndex === index) {\n return child;\n }\n }\n }\n\n return null;\n }\n\n /**\n * Evaluate a _simple XPath_ relative to a `root` element and return the\n * matching element.\n *\n * A _simple XPath_ is a sequence of one or more `/tagName[index]` strings.\n *\n * Unlike `document.evaluate` this function:\n *\n * - Only supports simple XPaths\n * - Is not affected by the document's _type_ (HTML or XML/XHTML)\n * - Ignores element namespaces when matching element names in the XPath against\n * elements in the DOM tree\n * - Is case insensitive for all elements, not just HTML elements\n *\n * The matching element is returned or `null` if no such element is found.\n * An error is thrown if `xpath` is not a simple XPath.\n *\n * @param {string} xpath\n * @param {Element} root\n * @return {Element|null}\n */\n function evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath = xpath.match(/^(\\/[A-Za-z0-9-]+(\\[[0-9]+\\])?)+$/) !== null;\n if (!isSimpleXPath) {\n throw new Error('Expression is not a simple XPath');\n }\n\n const segments = xpath.split('/');\n let element = root;\n\n // Remove leading empty segment. The regex above validates that the XPath\n // has at least two segments, with the first being empty and the others non-empty.\n segments.shift();\n\n for (let segment of segments) {\n let elementName;\n let elementIndex;\n\n const separatorPos = segment.indexOf('[');\n if (separatorPos !== -1) {\n elementName = segment.slice(0, separatorPos);\n\n const indexStr = segment.slice(separatorPos + 1, segment.indexOf(']'));\n elementIndex = parseInt(indexStr) - 1;\n if (elementIndex < 0) {\n return null;\n }\n } else {\n elementName = segment;\n elementIndex = 0;\n }\n\n const child = nthChildOfType(element, elementName, elementIndex);\n if (!child) {\n return null;\n }\n\n element = child;\n }\n\n return element;\n }\n\n /**\n * Finds an element node using an XPath relative to `root`\n *\n * Example:\n * node = nodeFromXPath('/main/article[1]/p[3]', document.body)\n *\n * @param {string} xpath\n * @param {Element} [root]\n * @return {Node|null}\n */\n function nodeFromXPath(xpath, root = document.body) {\n try {\n return evaluateSimpleXPath(xpath, root);\n } catch (err) {\n return document.evaluate(\n '.' + xpath,\n root,\n\n // The `namespaceResolver` and `result` arguments are optional in the spec\n // but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n }\n\n /**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\n function replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n }\n};"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startposition","endposition","text","type","html","css","color","insertBefore","show","focus","not","highlights","from","querySelectorAll","undefined","length","i","parentNode","pn","children","childNodes","replaceWith","normalize","removeHighlights","wholeTextNodesInRange","range","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","node","textNodes","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","startContainer","startOffset","splitText","endContainer","endOffset","data","push","highlightRange","cssClass","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","hihglightedtext","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","textContent","replaceChild","appendChild","_node$nodeValue","comparePoint","e","getPathSegment","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","xpathFromNode","xpath","elem","Error","replace","nthChildOfType","element","index","toUpperCase","matchIndex","child","evaluateSimpleXPath","match","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","nodeFromXPath","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","replacements","parent","r","remove","removeClass","on","preventDefault","keypress","which","this","parents","submit","selectedrange","window","getSelection","getRangeAt","cloneContents","annotatedtext","ajax","url","success","response","JSON","parse","Object","values","annotation","newrange","createRange","setStart","setEnd","recreateAnnotations","mouseenter","addClass","mouseleave","error","alert"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 7db3cf5..2d90df5 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -97,58 +97,50 @@ export const init = (cmid, canmakeannotations, myuserid) => { recreateAnnotations(); // Highlight annotation and all annotated text if annotated text is hovered - $('.annotated').mouseenter(function () { + $('.annotated').mouseenter(function() { var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).addClass('hovered'); + $('.annotation-box-' + id).addClass('hovered'); $('.annotated-' + id).addClass('hovered'); - $('.annotation-box-' + id + ' .errortype').addClass('hovered'); - }); - $('.annotated').mouseleave(function () { + $('.annotated').mouseleave(function() { var id = this.id.replace('annotated-', ''); - $('.annotationpreview-' + id).removeClass('hovered'); - $('.annotated-' + id).removeClass('hovered'); - $('.annotation-box-' + id + ' .errortype').removeClass('hovered'); - }); - - // Highlight annotated text if annotationpreview is hovered - $('.annotatedtextpreview').mouseenter(function () { - var id = this.id.replace('annotationpreview-', ''); - $('.annotated-' + id).addClass('hovered'); - }); - - $('.annotatedtextpreview').mouseleave(function () { - var id = this.id.replace('annotationpreview-', ''); + $('.annotation-box-' + id).removeClass('hovered'); $('.annotated-' + id).removeClass('hovered'); }); // Highlight whole temp annotation if part of temp annotation is hovered - $(document).on('mouseover', '.annotated_temp', function () { + $(document).on('mouseover', '.annotated_temp', function() { $('.annotated_temp').addClass('hovered'); }); - $(document).on('mouseleave', '.annotated_temp', function () { + $(document).on('mouseleave', '.annotated_temp', function() { $('.annotated_temp').removeClass('hovered'); }); // Onclick listener for editing annotation. - $(document).on('click', '.annotated', function () { + $(document).on('click', '.annotated', function() { var id = this.id.replace('annotated-', ''); editAnnotation(id); }); // Onclick listener for editing annotation. - $(document).on('click', '.edit-annotation', function () { + $(document).on('click', '.edit-annotation', function() { var id = this.id.replace('edit-annotation-', ''); editAnnotation(id); }); - // Onclick listener for click on annotation-box. - // $(document).on('click', '.annotation-box', function() { - // var id = this.id.replace('annotation-box-', ''); - // $('#annotated-' + id).focus(); - // }); + // Highlight annotation if hoverannotation button is hovered + $(document).on('mouseover', '.hoverannotation', function() { + var id = this.id.replace('hoverannotation-', ''); + $('.annotated-' + id).addClass('hovered'); + }); + + $(document).on('mouseleave', '.hoverannotation', function() { + var id = this.id.replace('hoverannotation-', ''); + $('.annotated-' + id).removeClass('hovered'); + }); + }, error: function() { alert ('Error fetiching annotations'); @@ -383,7 +375,6 @@ export const init = (cmid, canmakeannotations, myuserid) => { if (annotationid) { highlightEl.className += ' ' + cssClass + '-' + annotationid; - // highlightEl.tabIndex = 1; highlightEl.style = "text-decoration:underline; text-decoration-color: #" + color; highlightEl.id = cssClass + '-' + annotationid; highlightEl.style.backgroundColor = '#' + color; diff --git a/lang/de/margic.php b/lang/de/margic.php index c9a10fe..09a8d6b 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -269,6 +269,7 @@ {$a->teacher} hat eine Rückmeldung beziehungsweise Bewertung zu Ihrem Eintrag im Margic {$a->margic} veröffentlicht.

Hier können Sie diese ansehen.'; $string['mailfooter'] = 'Diese Nachricht bezieht sich auf ein Margic in {$a->systemname}. Unter dem folgenden Link finden Sie alle weiteren Informationen.
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; +$string['hoverannotation'] = 'Annotation hervorheben'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index bb62bad..068a524 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -282,6 +282,7 @@ {$a->teacher} has published a feedback or rating for your entry in Margic {$a->margic}.

Here you can view them.'; $string['mailfooter'] = 'This message is about a Margic in {$a->systemname}. You can find all further information under the following link:
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; +$string['hoverannotation'] = 'Hover annotation'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/styles.css b/styles.css index 043f1eb..815e61e 100644 --- a/styles.css +++ b/styles.css @@ -137,11 +137,6 @@ -moz-border-radius: 5px; } -.path-mod-margic .annotationarea .annotatedtextpreview:hover { - background-color: lightblue; - cursor: pointer; -} - .path-mod-margic .annotated, .path-mod-margic .annotated_temp { background-color: yellow; diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index fcd3ae5..be569e4 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -140,6 +140,7 @@ {{type}} +
From 0abc0e468a10639b7ef1f172b17e8d8ac3ac328d Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 4 Aug 2022 16:36:06 +0200 Subject: [PATCH 33/60] feat (print_recent_activity): added methods for printing recent activity, removed mailed property for entries and done minor bugfixes --- annotations.php | 8 +- backup/moodle2/backup_margic_stepslib.php | 2 +- classes/local/results.php | 4 - classes/search/entry.php | 2 +- db/install.xml | 1 - db/upgrade.php | 2 +- edit.php | 3 +- grade_entry.php | 1 - index.php | 2 +- lang/de/margic.php | 7 +- lang/en/margic.php | 7 +- lib.php | 288 +++++++++++++++++++--- settings.php | 19 -- 13 files changed, 261 insertions(+), 85 deletions(-) diff --git a/annotations.php b/annotations.php index 03ce345..3ebff70 100644 --- a/annotations.php +++ b/annotations.php @@ -70,7 +70,13 @@ // Get annotation (ajax). if ($getannotations) { - echo json_encode($margic->get_annotations()); + $annotations = $margic->get_annotations(); + if ($annotations) { + echo json_encode($annotations); + } else { + echo json_encode(array()); + } + die; } diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php index e4a1844..ac60884 100644 --- a/backup/moodle2/backup_margic_stepslib.php +++ b/backup/moodle2/backup_margic_stepslib.php @@ -50,7 +50,7 @@ protected function define_structure() { $entry = new backup_nested_element('entry', array('id'), array( 'userid', 'timecreated', 'timemodified', 'text', 'format', 'rating', 'entrycomment', 'formatcomment', 'teacher', - 'timemarked', 'mailed', 'baseentry')); + 'timemarked', 'baseentry')); $annotations = new backup_nested_element('annotations'); $annotation = new backup_nested_element('annotation', array('id'), array( diff --git a/classes/local/results.php b/classes/local/results.php index a278a4f..4fd21f2 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -224,7 +224,6 @@ public static function download_entries($context, $course, $margic) { get_string('entrycomment', 'margic'), get_string('teacher', 'margic'), get_string('timemarked', 'margic'), - get_string('mailed', 'margic'), get_string('baseentry', 'margic'), get_string('text', 'margic') ); @@ -244,7 +243,6 @@ public static function download_entries($context, $course, $margic) { d.entrycomment AS entrycomment, d.teacher AS teacher, to_char(to_timestamp(d.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked, - d.mailed AS mailed, d.baseentry AS baseentry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid @@ -263,7 +261,6 @@ public static function download_entries($context, $course, $margic) { d.entrycomment AS entrycomment, d.teacher AS teacher, FROM_UNIXTIME(d.timemarked) AS TIMEMARKED, - d.mailed AS mailed, d.baseentry AS baseentry FROM {margic_entries} d JOIN {user} u ON u.id = d.userid @@ -295,7 +292,6 @@ public static function download_entries($context, $course, $margic) { $d->entrycomment, $d->teacher, $d->timemarked, - $d->mailed, $d->baseentry, format_text($d->text, $d->format, array('para' => false)) ); diff --git a/classes/search/entry.php b/classes/search/entry.php index 24815de..fe25f5e 100644 --- a/classes/search/entry.php +++ b/classes/search/entry.php @@ -94,7 +94,7 @@ public function get_document($entry, $options = array()) { // Prepare associative array with data from DB. $doc = \core_search\document_factory::instance($entry->id, $this->componentname, $this->areaname); // I am using the entry date (timecreated) for the title. - $doc->set('title', content_to_text((date(get_config('mod_margic', 'dateformat'), $entry->timecreated)), $entry->format)); + $doc->set('title', content_to_text((userdate($entry->timecreated)), $entry->format)); $doc->set('content', content_to_text('Entry: ' . $entry->text, $entry->format)); $doc->set('contextid', $context->id); $doc->set('courseid', $entry->course); diff --git a/db/install.xml b/db/install.xml index 35b8c18..1f2216a 100644 --- a/db/install.xml +++ b/db/install.xml @@ -44,7 +44,6 @@ - diff --git a/db/upgrade.php b/db/upgrade.php index faa4b01..d3facce 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -110,7 +110,7 @@ function xmldb_margic_upgrade($oldversion) { // Add the baseentry field to the margic_entries table. $table = new xmldb_table('margic_entries'); - $field = new xmldb_field('baseentry', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'mailed'); + $field = new xmldb_field('baseentry', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'teacher'); // Conditionally launch add field. if (!$dbman->field_exists($table, $field)) { diff --git a/edit.php b/edit.php index 95c0cd2..67d66cf 100644 --- a/edit.php +++ b/edit.php @@ -137,8 +137,7 @@ $data->textformat = $entry->format; $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', - array('annotations' => $margic->get_annotations(), - 'canmakeannotations' => false)); + array('cmid' => $cm->id, 'canmakeannotations' => false, 'myuserid' => $USER->id)); } else { $entry = false; diff --git a/grade_entry.php b/grade_entry.php index a7fc833..784b393 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -135,7 +135,6 @@ $entry->formatcomment = $fromform->{'feedback_' . $entry->id . '_editor'}['format']; $entry->teacher = $USER->id; $entry->timemarked = $timenow; - $entry->mailed = 0; // Make sure mail goes out (again). if (!$DB->update_record("margic_entries", $entry)) { redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('errfeedbacknotupdated', 'mod_margic'), null, notification::NOTIFY_ERROR); diff --git a/index.php b/index.php index 1f7ee2c..918c0ff 100644 --- a/index.php +++ b/index.php @@ -114,7 +114,7 @@ } // Description. - $table->data[$i][] = format_text($margic->intro, $margic->introformat); + $table->data[$i][] = format_module_intro('margic', $margic, $margic->coursemodule); $i ++; } diff --git a/lang/de/margic.php b/lang/de/margic.php index 09a8d6b..a0036ea 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -32,10 +32,8 @@ $string['blankentry'] = 'Leerer Eintrag'; $string['calendarend'] = '{$a} schließt'; $string['calendarstart'] = '{$a} öffnet'; -$string['configdateformat'] = 'Damit wird festgelegt, wie Daten in Margic-Berichten angezeigt werden. Der Standardwert "M d, Y G:i" ist Monat, Tag, Jahr und Uhrzeit im 24-Stunden-Format. Weitere Beispiele und vordefinierte Datumskonstanten finden Sie unter Datum im PHP-Handbuch.'; $string['created'] = 'vor {$a->years} Jahren, {$a->month} Monaten, {$a->days} Tagen und {$a->hours} Stunden'; $string['csvexport'] = 'Exportieren nach .csv'; -$string['dateformat'] = 'Standard-Datumsformat'; $string['deadline'] = 'Offene Tage'; $string['details'] = 'Statistik'; $string['margic:addentries'] = 'Margic-Einträge hinzufügen'; @@ -90,7 +88,7 @@ $string['modulenameplural'] = 'Margics'; $string['needsgrading'] = 'Dieser Eintrag hat noch keine Rückmeldung oder Bewertung erhalten.'; $string['needsregrading'] = 'Dieser Eintrag hat sich geändert, seit ein Feedback oder eine Bewertung abgegeben wurde.'; -$string['newmargicentries'] = 'Neue Margic-Einträge'; +$string['neworeditedmargicentries'] = 'Neue oder bearbeitete Margic-Einträge'; $string['nextentry'] = 'Nächster Eintrag'; $string['nodeadline'] = 'Immer offen'; $string['noentriesmanagers'] = 'Keine Trainer/innen'; @@ -114,8 +112,6 @@ $string['search:entry'] = 'Margic-Einträge'; $string['search:entrycomment'] = 'Kommentar zum Margic-Eintrag'; $string['selectentry'] = 'Eintrag zur Kennzeichnung auswählen'; -$string['showoverview'] = 'Margic-Übersicht im Dashboard'; -$string['showrecentactivity'] = 'Aktuelle Aktivität anzeigen'; $string['sortcurrententry'] = 'Vom aktuellen Margic-Eintrag bis zum ersten.'; $string['sorthighestentry'] = 'Vom am höchsten bewerteten Margic-Eintrag bis zum am niedrigsten bewerteten.'; $string['sortlastentry'] = 'Vom zuletzt geänderten Margic-Eintrag bis zum ältesten geänderten.'; @@ -260,7 +256,6 @@ $string['timecreatedinvalid'] = 'Änderung fehlgeschlagen. Es gibt bereits jüngere Versionen dieses Beitrags.'; $string['messageprovider:gradingmessages'] = 'Systemnachrichten bei der Bewertung von Einträgen'; $string['sendgradingmessage'] = 'Ersteller/in des Eintrags sofort über die Bewertung benachrichtigen'; -$string['mailed'] = 'Benachrichtigt'; $string['gradingmailsubject'] = 'Feedback zu Margic-Eintrag erhalten'; $string['gradingmailfullmessage'] = 'Hallo {$a->user}, {$a->teacher} hat eine Rückmeldung beziehungsweise Bewertung zu Ihrem Eintrag im Margic {$a->margic} veröffentlicht. diff --git a/lang/en/margic.php b/lang/en/margic.php index 068a524..084967e 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -44,11 +44,9 @@ $string['blankentry'] = 'Blank entry'; $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; -$string['configdateformat'] = 'This defines how dates are shown in margic reports. The default value, "M d, Y G:i" is Month, day, year and 24 hour format time. Refer to Date in the PHP manual for more examples and predefined date constants.'; $string['created'] = '{$a->years} years, {$a->month} months, {$a->days} days and {$a->hours} hours ago'; $string['csvexport'] = 'Export to .csv'; $string['deadline'] = 'Days Open'; -$string['dateformat'] = 'Default date format'; $string['details'] = 'Statistics'; $string['margicclosetime'] = 'Close time'; $string['margicclosetime_help'] = 'If this option is activated, you can set a date on which the Margic is closed. Participants will no longer be able to create or edit entries after that date.'; @@ -104,7 +102,7 @@ $string['modulenameplural'] = 'Margics'; $string['needsgrading'] = ' This entry has not been given feedback or a rating yet.'; $string['needsregrading'] = 'This entry has changed since feedback or a rating was given.'; -$string['newmargicentries'] = 'New margic entries'; +$string['neworeditedmargicentries'] = 'New or edited margic entries'; $string['nextentry'] = 'Next entry'; $string['nodeadline'] = 'Always open'; $string['noentriesmanagers'] = 'There are no teachers'; @@ -129,8 +127,6 @@ $string['search:entrycomment'] = 'margic - entry comment'; $string['search:activity'] = 'margic - activity information'; $string['selectentry'] = 'Select entry for marking'; -$string['showrecentactivity'] = 'Show recent activity'; -$string['showoverview'] = 'Show margics overview on my moodle'; $string['sortorder'] = 'Sort order is: '; $string['sortcurrententry'] = 'From current margic entry to the first entry.'; $string['sortlowestentry'] = 'From lowest rated margic entry to the highest entry.'; @@ -273,7 +269,6 @@ $string['timecreatedinvalid'] = 'Change failed. There are already younger versions of this entry.'; $string['messageprovider:gradingmessages'] = 'Notifications when entries are rated'; $string['sendgradingmessage'] = 'Notify the creator of the entry immediately about the rating'; -$string['mailed'] = 'Mailed'; $string['gradingmailsubject'] = 'Received feedback for Margic entry'; $string['gradingmailfullmessage'] = 'Greetings {$a->user}, {$a->teacher} has published a feedback or rating for your entry in Margic {$a->margic}. diff --git a/lib.php b/lib.php index 3c6fd61..25df469 100644 --- a/lib.php +++ b/lib.php @@ -340,14 +340,12 @@ function margic_user_complete($course, $user, $mod, $margic) { * @param int $timestart * @return bool */ -/* function margic_print_recent_activity($course, $viewfullnames, $timestart) { +function margic_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT; - if (! get_config('margic', 'showrecentactivity')) { - return false; - } + error_log('margic_print_recent_activity'); - $dbparams = array( + $params = array( $timestart, $course->id, 'margic' @@ -359,36 +357,36 @@ function margic_user_complete($course, $user, $mod, $margic) { $userfieldsapi = \core_user\fields::for_userpic(); $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; } - $sql = "SELECT de.id, de.timemodified, cm.id AS cmid, $namefields - FROM {margic_entries} de - JOIN {margic} d ON d.id = de.margic + $sql = "SELECT e.id, e.timemodified, cm.id AS cmid, $namefields + FROM {margic_entries} e + JOIN {margic} d ON d.id = e.margic JOIN {course_modules} cm ON cm.instance = d.id JOIN {modules} md ON md.id = cm.module - JOIN {user} u ON u.id = de.userid - WHERE de.timemodified > ? AND d.course = ? AND md.name = ? + JOIN {user} u ON u.id = e.userid + WHERE e.timemodified > ? AND d.course = ? AND md.name = ? ORDER BY u.lastname ASC, u.firstname ASC "; - // Changed on 20190622 original line 310: ORDER BY de.timemodified ASC. - $newentries = $DB->get_records_sql($sql, $dbparams); + + $newentries = $DB->get_records_sql($sql, $params); $modinfo = get_fast_modinfo($course); $show = array(); - foreach ($newentries as $anentry) { - if (! array_key_exists($anentry->cmid, $modinfo->get_cms())) { + foreach ($newentries as $entry) { + if (! array_key_exists($entry->cmid, $modinfo->get_cms())) { continue; } - $cm = $modinfo->get_cm($anentry->cmid); + $cm = $modinfo->get_cm($entry->cmid); if (! $cm->uservisible) { continue; } - if ($anentry->userid == $USER->id) { - $show[] = $anentry; + if ($entry->userid == $USER->id) { + $show[] = $entry; continue; } - $context = context_module::instance($anentry->cmid); + $context = context_module::instance($entry->cmid); // Only teachers can see other students entries. if (! has_capability('mod/margic:manageentries', $context)) { @@ -407,7 +405,7 @@ function margic_user_complete($course, $user, $mod, $margic) { if (! $modinfo->get_groups($cm->groupingid)) { continue; } - $usersgroups = groups_get_all_groups($course->id, $anentry->userid, $cm->groupingid); + $usersgroups = groups_get_all_groups($course->id, $entry->userid, $cm->groupingid); if (is_array($usersgroups)) { $usersgroups = array_keys($usersgroups); $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); @@ -416,27 +414,214 @@ function margic_user_complete($course, $user, $mod, $margic) { } } } - $show[] = $anentry; + $show[] = $entry; } if (empty($show)) { return false; } - echo $OUTPUT->heading(get_string('newmargicentries', 'margic') . ':', 3); + echo $OUTPUT->heading(get_string('neworeditedmargicentries', 'margic') . ':', 6); - foreach ($show as $submission) { - $cm = $modinfo->get_cm($submission->cmid); - $context = context_module::instance($submission->cmid); - if (has_capability('mod/margic:manageentries', $context)) { - $link = $CFG->wwwroot . '/mod/margic/report.php?id=' . $cm->id; - } else { - $link = $CFG->wwwroot . '/mod/margic/view.php?id=' . $cm->id; - } - print_recent_activity_note($submission->timemodified, $submission, $cm->name, $link, false, $viewfullnames); + foreach ($show as $entry) { + $cm = $modinfo->get_cm($entry->cmid); + $context = context_module::instance($entry->cmid); + $link = $CFG->wwwroot . '/mod/margic/view.php?id=' . $cm->id; + print_recent_activity_note($entry->timemodified, $entry, $cm->name, $link, false, $viewfullnames); + echo '
'; } + return true; -} */ +} + +/** + * Returns all margics since a given time. + * + * @param array $activities The activity information is returned in this array + * @param int $index The current index in the activities array + * @param int $timestart The earliest activity to show + * @param int $courseid Limit the search to this course + * @param int $cmid The course module id + * @param int $userid Optional user id + * @param int $groupid Optional group id + * @return void + */ +function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $courseid, + $cmid, $userid=0, $groupid=0) { + + global $CFG, $COURSE, $USER, $DB; + + error_log('margic_get_recent_mod_activity'); + + if ($COURSE->id == $courseid) { + $course = $COURSE; + } else { + $course = $DB->get_record('course', array('id'=>$courseid)); + } + + $modinfo = get_fast_modinfo($course); + + $cm = $modinfo->get_cm($cmid); + $params = array(); + if ($userid) { + $userselect = 'AND u.id = :userid'; + $params['userid'] = $userid; + } else { + $userselect = ''; + } + + if ($groupid) { + $groupselect = 'AND gm.groupid = :groupid'; + $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; + $params['groupid'] = $groupid; + } else { + $groupselect = ''; + $groupjoin = ''; + } + + $params['cminstance'] = $cm->instance; + $params['timestart'] = $timestart; + $params['submitted'] = 1; + + $userfields = user_picture::fields('u', null, 'userid'); + + $entries = $DB->get_records_sql( + 'SELECT e.id, e.timemodified, ' . $userfields . + ' FROM {margic_entries} e + JOIN {margic} m ON m.id = e.margic + JOIN {user} u ON u.id = e.userid ' . $groupjoin . + ' WHERE e.timemodified > :timestart AND + m.id = :cminstance + ' . $userselect . ' ' . $groupselect . + ' ORDER BY e.timemodified ASC', $params); + + if (!$entries) { + return; + } + + $groupmode = groups_get_activity_groupmode($cm, $course); + $cmcontext = context_module::instance($cm->id); + $grader = has_capability('moodle/grade:viewall', $cmcontext); + $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext); + $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); + + $show = array(); + foreach ($entries as $entry) { + if ($entry->userid == $USER->id) { + $show[] = $entry; + continue; + } + + if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { + if (isguestuser()) { + // Shortcut - guest user does not belong into any group. + continue; + } + + // This will be slow - show only users that share group with me in this cm. + if (!$modinfo->get_groups($cm->groupingid)) { + continue; + } + $usersgroups = groups_get_all_groups($course->id, $entry->userid, $cm->groupingid); + if (is_array($usersgroups)) { + $usersgroups = array_keys($usersgroups); + $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid)); + if (empty($intersect)) { + continue; + } + } + } + $show[] = $entry; + } + + if (empty($show)) { + return; + } + + if ($grader) { + require_once($CFG->libdir.'/gradelib.php'); + $userids = array(); + foreach ($show as $id => $entry) { + $userids[] = $entry->userid; + } + $grades = grade_get_grades($courseid, 'mod', 'margic', $cm->instance, $userids); + } + + $aname = format_string($cm->name, true); + foreach ($show as $entry) { + $activity = new stdClass(); + + $activity->type = 'margic'; + $activity->cmid = $cm->id; + $activity->name = $aname; + $activity->sectionnum = $cm->sectionnum; + $activity->timestamp = $entry->timemodified; + $activity->user = new stdClass(); + if ($grader) { + $activity->grade = $grades->items[0]->grades[$entry->userid]->str_long_grade; + } + + $userfields = explode(',', user_picture::fields()); + foreach ($userfields as $userfield) { + if ($userfield == 'id') { + // Aliased in SQL above. + $activity->user->{$userfield} = $entry->userid; + } else { + $activity->user->{$userfield} = $entry->{$userfield}; + } + } + $activity->user->fullname = fullname($entry, $viewfullnames); + + $activities[$index++] = $activity; + } + + return; +} + +/** + * Print recent activity from all margics in a given course + * + * This is used by course/recent.php + * @param stdClass $activity + * @param int $courseid + * @param bool $detail + * @param array $modnames + */ +function margic_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { + global $CFG, $OUTPUT; + + error_log('margic_print_recent_mod_activity'); + + echo ''; + + echo '
'; + echo $OUTPUT->user_picture($activity->user); + echo ''; + + if ($detail) { + $modname = $modnames[$activity->type]; + echo '
'; + echo $OUTPUT->image_icon('icon', $modname, 'margic'); + echo ''; + echo $activity->name; + echo ''; + echo '
'; + } + + if (isset($activity->grade)) { + echo '
'; + echo get_string('grade').': '; + echo $activity->grade; + echo '
'; + } + + echo ''; + + echo '
'; +} /** * Implementation of the function for printing the form elements that control @@ -819,19 +1004,40 @@ function margic_scale_used_anywhere($scaleid) { } */ /** - * Returns the margic instance course_module id. + * Add a get_coursemodule_info function in case any assignment type wants to add 'extra' information + * for the course (see resource). * - * @param integer $margicid - * @return object + * Given a course_module object, this function returns any "extra" information that may be needed + * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. + * + * @param stdClass $coursemodule The coursemodule object (record). + * @return cached_cm_info An object on information that the courses + * will know about (most noticeably, an icon). */ -/* function margic_get_coursemodule($margicid) { - global $DB; +/* function margic_get_coursemodule_info($coursemodule) { + global $CFG, $DB; - return $DB->get_record_sql("SELECT cm.id FROM {course_modules} cm - JOIN {modules} m ON m.id = cm.module - WHERE cm.instance = ? AND m.name = 'margic'", array( - $margicid - )); + $dbparams = array('id'=>$coursemodule->instance); + $fields = 'id, name, alwaysshowdescription, allowsubmissionsfromdate, intro, introformat, completionsubmit'; + if (! $assignment = $DB->get_record('assign', $dbparams, $fields)) { + return false; + } + + $result = new cached_cm_info(); + $result->name = $assignment->name; + if ($coursemodule->showdescription) { + if ($assignment->alwaysshowdescription || time() > $assignment->allowsubmissionsfromdate) { + // Convert intro to html. Do not filter cached version, filters run at display time. + $result->content = format_module_intro('assign', $assignment, $coursemodule->id, false); + } + } + + // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. + if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { + $result->customdata['customcompletionrules']['completionsubmit'] = $assignment->completionsubmit; + } + + return $result; } */ /** diff --git a/settings.php b/settings.php index 2540bb6..10314c7 100644 --- a/settings.php +++ b/settings.php @@ -28,20 +28,6 @@ // Availability settings. $settings->add(new admin_setting_heading('mod_margic/availibility', get_string('availability'), '')); - $settings->add(new admin_setting_configselect('margic/showrecentactivity', - get_string('showrecentactivity', 'margic'), - get_string('showrecentactivity', 'margic'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); - - $settings->add(new admin_setting_configselect('margic/overview', - get_string('showoverview', 'margic'), - get_string('showoverview', 'margic'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); - // 20201015 Default edit all entries setting. $settings->add(new admin_setting_configselect('margic/editall', get_string('editall', 'margic'), @@ -66,11 +52,6 @@ $settings->add(new admin_setting_configtext('mod_margic/annotationareawidth', get_string('annotationareawidth', 'margic'), get_string('annotationareawidthall', 'margic'), 40, '/^([2-7]\d|80)+$/')); // Range allowed: 20-80 - // Date format setting. - $settings->add(new admin_setting_configtext('mod_margic/dateformat', - get_string('dateformat', 'margic'), - get_string('configdateformat', 'margic'), 'M d, Y G:i', PARAM_TEXT, 15)); - // margic entry/feedback background colour setting. $name = 'mod_margic/entrybgc'; $title = get_string('entrybgc_title', 'margic'); From a7cb2ae518e6ae2ce116cd9bc760bef52276fb38 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 4 Aug 2022 17:58:43 +0200 Subject: [PATCH 34/60] feat (print_recent_activity): small fixes and improvements --- edit.php | 14 ++++++++++---- lang/de/margic.php | 5 +++-- lang/en/margic.php | 5 +++-- lib.php | 44 +++++++++++++++++++++++++------------------- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/edit.php b/edit.php index 67d66cf..a268740 100644 --- a/edit.php +++ b/edit.php @@ -168,8 +168,14 @@ $newentry->margic = $moduleinstance->id; $newentry->userid = $USER->id; - $newentry->timecreated = $fromform->timecreated; - $newentry->timemodified = $fromform->timecreated; + if ($moduleinstance->editdates) { + $newentry->timecreated = $fromform->timecreated; + $newentry->timemodified = $fromform->timecreated; + } else { + $newentry->timecreated = $timenow; + $newentry->timemodified = $timenow; + } + $newentry->text = ''; $newentry->format = 1; @@ -239,10 +245,10 @@ $event->trigger(); if ($moduleinstance->editdates && $fromform->timecreated > $timenow) { - redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryadded', 'mod_margic') . + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryaddedoredited', 'mod_margic') . ' ' . get_string('editdateinfuture', 'mod_margic'), null, notification::NOTIFY_WARNING); } else { - redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); + redirect(new moodle_url('/mod/margic/view.php?id=' . $cm->id), get_string('entryaddedoredited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } } diff --git a/lang/de/margic.php b/lang/de/margic.php index a0036ea..8e63272 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -88,7 +88,7 @@ $string['modulenameplural'] = 'Margics'; $string['needsgrading'] = 'Dieser Eintrag hat noch keine Rückmeldung oder Bewertung erhalten.'; $string['needsregrading'] = 'Dieser Eintrag hat sich geändert, seit ein Feedback oder eine Bewertung abgegeben wurde.'; -$string['neworeditedmargicentries'] = 'Neue oder bearbeitete Margic-Einträge'; +$string['newmargicentries'] = 'Neue Margic-Einträge'; $string['nextentry'] = 'Nächster Eintrag'; $string['nodeadline'] = 'Immer offen'; $string['noentriesmanagers'] = 'Keine Trainer/innen'; @@ -224,7 +224,7 @@ $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; $string['toggleannotation'] = 'Annotation aus- / einklappen'; $string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; -$string['entryadded'] = 'Eintrag angelegt oder bearbeitet'; +$string['entryaddedoredited'] = 'Eintrag angelegt oder bearbeitet'; $string['deletealluserdata'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags löschen'; $string['alluserdatadeleted'] = 'Alle Einträge, deren Annotationen, Dateien, Bewertungen und Tags wurden entfernt'; $string['deleteerrortypes'] = 'Fehlertypen löschen'; @@ -265,6 +265,7 @@ Hier können Sie diese ansehen.'; $string['mailfooter'] = 'Diese Nachricht bezieht sich auf ein Margic in {$a->systemname}. Unter dem folgenden Link finden Sie alle weiteren Informationen.
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; $string['hoverannotation'] = 'Annotation hervorheben'; +$string['entryadded'] = 'Eintrag angelegt'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 084967e..8b62a6b 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -102,7 +102,7 @@ $string['modulenameplural'] = 'Margics'; $string['needsgrading'] = ' This entry has not been given feedback or a rating yet.'; $string['needsregrading'] = 'This entry has changed since feedback or a rating was given.'; -$string['neworeditedmargicentries'] = 'New or edited margic entries'; +$string['newmargicentries'] = 'New margic entries'; $string['nextentry'] = 'Next entry'; $string['nodeadline'] = 'Always open'; $string['noentriesmanagers'] = 'There are no teachers'; @@ -237,7 +237,7 @@ $string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; $string['toggleannotation'] = 'Toggle annotation'; $string['toggleallannotations'] = 'Toggle all annotations'; -$string['entryadded'] = 'Entry added or modified.'; +$string['entryaddedoredited'] = 'Entry added or modified.'; $string['deletealluserdata'] = 'Delete all entries, annotations, files, ratings and tags'; $string['alluserdatadeleted'] = 'All entries, annotations, files, ratings and tags are deleted'; $string['deleteerrortypes'] = 'Delete errortypes'; @@ -278,6 +278,7 @@ Here you can view them.'; $string['mailfooter'] = 'This message is about a Margic in {$a->systemname}. You can find all further information under the following link:
{$a->coursename} -> Margic -> {$a->name}
{$a->url}'; $string['hoverannotation'] = 'Hover annotation'; +$string['entryadded'] = 'Entry added'; // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; diff --git a/lib.php b/lib.php index 25df469..66eba30 100644 --- a/lib.php +++ b/lib.php @@ -343,8 +343,6 @@ function margic_user_complete($course, $user, $mod, $margic) { function margic_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT; - error_log('margic_print_recent_activity'); - $params = array( $timestart, $course->id, @@ -357,14 +355,14 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { $userfieldsapi = \core_user\fields::for_userpic(); $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; } - $sql = "SELECT e.id, e.timemodified, cm.id AS cmid, $namefields + $sql = "SELECT e.id, e.timecreated, cm.id AS cmid, $namefields FROM {margic_entries} e JOIN {margic} d ON d.id = e.margic JOIN {course_modules} cm ON cm.instance = d.id JOIN {modules} md ON md.id = cm.module JOIN {user} u ON u.id = e.userid - WHERE e.timemodified > ? AND d.course = ? AND md.name = ? - ORDER BY u.lastname ASC, u.firstname ASC + WHERE e.timecreated > ? AND d.course = ? AND md.name = ? + ORDER BY timecreated DESC "; $newentries = $DB->get_records_sql($sql, $params); @@ -388,8 +386,10 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { } $context = context_module::instance($entry->cmid); + $teacher = has_capability('mod/margic:manageentries', $context); + // Only teachers can see other students entries. - if (! has_capability('mod/margic:manageentries', $context)) { + if (!$teacher) { continue; } @@ -421,13 +421,13 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { return false; } - echo $OUTPUT->heading(get_string('neworeditedmargicentries', 'margic') . ':', 6); + echo $OUTPUT->heading(get_string('newmargicentries', 'margic') . ':', 6); foreach ($show as $entry) { $cm = $modinfo->get_cm($entry->cmid); $context = context_module::instance($entry->cmid); $link = $CFG->wwwroot . '/mod/margic/view.php?id=' . $cm->id; - print_recent_activity_note($entry->timemodified, $entry, $cm->name, $link, false, $viewfullnames); + print_recent_activity_note($entry->timecreated, $entry, $cm->name, $link, false, $viewfullnames); echo '
'; } @@ -451,8 +451,6 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour global $CFG, $COURSE, $USER, $DB; - error_log('margic_get_recent_mod_activity'); - if ($COURSE->id == $courseid) { $course = $COURSE; } else { @@ -486,14 +484,14 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $userfields = user_picture::fields('u', null, 'userid'); $entries = $DB->get_records_sql( - 'SELECT e.id, e.timemodified, ' . $userfields . + 'SELECT e.id, e.timecreated, ' . $userfields . ' FROM {margic_entries} e JOIN {margic} m ON m.id = e.margic JOIN {user} u ON u.id = e.userid ' . $groupjoin . - ' WHERE e.timemodified > :timestart AND + ' WHERE e.timecreated > :timestart AND m.id = :cminstance ' . $userselect . ' ' . $groupselect . - ' ORDER BY e.timemodified ASC', $params); + ' ORDER BY e.timecreated DESC', $params); if (!$entries) { return; @@ -504,6 +502,7 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $grader = has_capability('moodle/grade:viewall', $cmcontext); $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext); $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); + $teacher = has_capability('mod/margic:manageentries', $cmcontext); $show = array(); foreach ($entries as $entry) { @@ -512,6 +511,11 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour continue; } + // Only teachers can see other students entries. + if (!$teacher) { + continue; + } + if ($groupmode == SEPARATEGROUPS and !$accessallgroups) { if (isguestuser()) { // Shortcut - guest user does not belong into any group. @@ -555,7 +559,7 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $activity->cmid = $cm->id; $activity->name = $aname; $activity->sectionnum = $cm->sectionnum; - $activity->timestamp = $entry->timemodified; + $activity->timestamp = $entry->timecreated; $activity->user = new stdClass(); if ($grader) { $activity->grade = $grades->items[0]->grades[$entry->userid]->str_long_grade; @@ -590,8 +594,6 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour function margic_print_recent_mod_activity($activity, $courseid, $detail, $modnames) { global $CFG, $OUTPUT; - error_log('margic_print_recent_mod_activity'); - echo ''; echo '
'; @@ -608,16 +610,20 @@ function margic_print_recent_mod_activity($activity, $courseid, $detail, $modnam echo ''; } - if (isset($activity->grade)) { + /* if (isset($activity->grade)) { echo '
'; echo get_string('grade').': '; echo $activity->grade; echo '
'; - } + } */ + + echo ''; echo '
'; echo "wwwroot/user/view.php?id={$activity->user->id}&course=$courseid\">"; - echo "{$activity->user->fullname} - " . userdate($activity->timestamp); + echo "{$activity->user->fullname} - " . userdate($activity->timestamp); echo '
'; echo '
'; From 7f21e4b6a5aaccc7b839d43335273906d31bb111 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 4 Aug 2022 20:18:23 +0200 Subject: [PATCH 35/60] feat (settings): some rework for admin settings --- classes/local/results.php | 2 +- classes/output/margic_entry.php | 6 ++--- classes/output/margic_view.php | 10 ++++----- db/install.xml | 2 +- lang/de/margic.php | 33 +++++++++++++++------------- lang/en/margic.php | 27 +++++++++++++---------- locallib.php | 2 +- mod_form.php | 30 +++++++++++++++---------- settings.php | 28 +++++++++++------------ templates/margic_childentry.mustache | 6 ++--- templates/margic_entry.mustache | 8 +++---- view.php | 2 +- 12 files changed, 85 insertions(+), 71 deletions(-) diff --git a/classes/local/results.php b/classes/local/results.php index 4fd21f2..8e33017 100644 --- a/classes/local/results.php +++ b/classes/local/results.php @@ -544,7 +544,7 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $feedbackarea .= $mform->render(); } else if ($feedbacktext || ! empty($entry->rating)) { // If user is student and has rating or feedback text. - $feedbackarea .= '
'; + $feedbackarea .= '
'; $feedbackarea .= '
' . get_string('feedback') . ' ' . get_string('from', 'mod_margic') . ' ' . $teacherimage . ' '; $feedbackarea .= get_string('at', 'mod_margic') . ' ' . userdate($entry->timemarked) . ''; diff --git a/classes/output/margic_entry.php b/classes/output/margic_entry.php index 8e6bb56..04beba7 100644 --- a/classes/output/margic_entry.php +++ b/classes/output/margic_entry.php @@ -55,7 +55,7 @@ class margic_entry implements renderable, templatable { /** @var string */ protected $entrybgc; /** @var string */ - protected $entrytextbgc; + protected $textbgc; /** @var int */ protected $entryareawidth; /** @var int */ @@ -181,8 +181,8 @@ public function export_for_template(renderer_base $output) { $data->singleuser = $this->singleuser; $data->annotationmode = $this->annotationmode; $data->canmakeannotations = $this->canmakeannotations; - $data->entrybgc = get_config('mod_margic', 'entrybgc'); - $data->entrytextbgc = get_config('mod_margic', 'entrytextbgc'); + $data->entrybgc = get_config('margic', 'entrybgc'); + $data->textbgc = get_config('margic', 'textbgc'); $data->errortypes = $this->errortypes; $data->readonly = $this->readonly; return $data; diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php index 55bb432..3ee8e15 100644 --- a/classes/output/margic_view.php +++ b/classes/output/margic_view.php @@ -57,7 +57,7 @@ class margic_view implements renderable, templatable { /** @var string */ protected $entrybgc; /** @var string */ - protected $entrytextbgc; + protected $textbgc; /** @var int */ protected $entryareawidth; /** @var int */ @@ -105,7 +105,7 @@ class margic_view implements renderable, templatable { * @param array $entries The accessible entries for the margic instance * @param string $sortmode Sort mode for the margic instance * @param string $entrybgc Background color of the entries - * @param string $entrytextbgc Background color of the texts in the entries + * @param string $textbgc Background color of the texts in the entries * @param int $entryareawidth Width of the entry area * @param int $annotationareawidth Width of the annotation area * @param bool $caneditentries If own entries can be edited @@ -126,7 +126,7 @@ class margic_view implements renderable, templatable { * @param bool $canmakeannotations If user can make annotations * @param array $errortypes Array with annotation types for form */ - public function __construct($margic, $cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $entrytextbgc, $annotationareawidth, + public function __construct($margic, $cm, $context, $moduleinstance, $entries, $sortmode, $entrybgc, $textbgc, $annotationareawidth, $caneditentries, $edittimestarts, $edittimenotstarted, $edittimeends, $edittimehasended, $canmanageentries, $sesskey, $currentuserrating, $ratingaggregationmode, $course, $singleuser, $pagecountoptions, $pagebar, $entriescount, $annotationmode, $canmakeannotations, $errortypes) { @@ -139,7 +139,7 @@ public function __construct($margic, $cm, $context, $moduleinstance, $entries, $ $this->entries = $entries; $this->sortmode = $sortmode; $this->entrybgc = $entrybgc; - $this->entrytextbgc = $entrytextbgc; + $this->textbgc = $textbgc; $this->annotationareawidth = $annotationareawidth; $this->entryareawidth = 100 - $annotationareawidth; $this->caneditentries = $caneditentries; @@ -206,7 +206,7 @@ public function export_for_template(renderer_base $output) { $data->entries = $this->entries; $data->sortmode = $this->sortmode; $data->entrybgc = $this->entrybgc; - $data->entrytextbgc = $this->entrytextbgc; + $data->textbgc = $this->textbgc; $data->entryareawidth = $this->entryareawidth; $data->annotationareawidth = $this->annotationareawidth; $data->caneditentries = $this->caneditentries; diff --git a/db/install.xml b/db/install.xml index 1f2216a..27371a6 100644 --- a/db/install.xml +++ b/db/install.xml @@ -19,7 +19,7 @@ - + diff --git a/lang/de/margic.php b/lang/de/margic.php index 8e63272..bfa23f3 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -50,23 +50,13 @@ $string['margicdescription'] = 'Beschreibung der Margic-Instanz'; $string['margicopentime'] = 'Startzeit'; $string['margicopentime_help'] = 'Wenn diese Option aktiviert ist, können Sie ein Datum festlegen, an dem die Margic-Instanz zur Verwendung geöffnet wird.'; -$string['editall'] = 'Eigene Einträge bearbeiten'; -$string['editall_help'] = 'Wenn aktiviert, können Nutzer/innen alle eigenen Einträge in einem Margic bearbeiten.'; -$string['editdates'] = 'Eintragsdatum bearbeiten'; -$string['editdates_help'] = 'Wenn aktiviert, können Nutzer/innen das Datum jedes Eintrags bearbeiten.'; $string['editingends'] = 'Bearbeitungszeitraum beginnt am {$a}'; $string['editingended'] = 'Die Bearbeitungszeit endete am {$a}'; $string['editingends'] = 'Bearbeitungszeitraum endet am {$a}'; $string['editthisentry'] = 'Diesen Eintrag bearbeiten'; $string['entries'] = 'Einträge'; $string['entry'] = 'Eintrag'; -$string['entrybgc_colour'] = '#C8E5FD'; -$string['entrybgc_descr'] = 'Hier wird die Hintergrundfarbe eines Margic-Eintrages bzw. eines Feedbacks festgelegt.'; -$string['entrybgc_title'] = 'Hintergrundfarbe für Margic-Einträge und Feedback'; $string['entrycomment'] = 'Kommentar zum Eintrag'; -$string['entrytextbgc_colour'] = '#F9F5F0'; -$string['entrytextbgc_descr'] = 'Hiermit wird die Hintergrundfarbe des Textes in einem Margic-Eintrag festgelegt.'; -$string['entrytextbgc_title'] = 'Hintergrundfarbe des Textes'; $string['eventmargiccreated'] = 'Margic erstellt'; $string['eventmargicdeleted'] = 'Margic gelöscht'; $string['eventmargicviewed'] = 'Margic angezeigt'; @@ -131,7 +121,7 @@ $string['startnewentry'] = 'Neuer Eintrag'; $string['viewentries'] = 'Einträge ansehen'; $string['numwordsraw'] = '{$a->wordscount} Wörter mit {$a->charscount} Zeichen, einschließlich {$a->spacescount} Leerzeichen. '; -$string['margicentrydate'] = 'Datum des Eintrages bestimmen'; +$string['margicentrydate'] = 'Datum des Eintrags bestimmen'; $string['margic:viewannotations'] = 'Annotationen ansehen'; $string['margic:makeannotations'] = 'Annotationen anlegen'; @@ -154,7 +144,7 @@ $string['startoreditentry'] = 'Eintrag anlegen oder bearbeiten'; $string['addnewentry'] = 'Neuen Eintrag anlegen'; $string['editentry'] = 'Eintrag bearbeiten'; -$string['editentrynotpossible'] = 'Bearbeiten des Eintrages nicht möglich.'; +$string['editentrynotpossible'] = 'Bearbeiten des Eintrags nicht möglich.'; $string['editdateinfuture'] = 'Das angegebene Erstelldatum des Eintrags liegt in der Zukunft.'; $string['currenttooldest'] = 'Zeige die Einträge vom Aktuellsten zum Ältesten'; $string['oldesttocurrent'] = 'Zeige die Einträge vom Ältesten zum Aktuellsten'; @@ -218,9 +208,7 @@ $string['errfeedbacknotupdated'] = 'Rückmeldung und Note konnte nicht aktualisiert werden'; $string['errnograder'] = 'Kein Bewerter.'; $string['errnofeedbackorratingdisabled'] = 'Keine Rückmeldung oder Rückmeldung ist deaktiviert.'; -$string['annotationareawidth'] = 'Breite des Annotationsbereichs'; -$string['annotationareawidthall'] = 'Die Breite des Annotationsbereiches in Prozent für alle Margics. Kann von Lehrenden in den einzelnen Margics überschrieben werden.'; -$string['annotationareawidth_help'] = 'Die Breite des Annotationsbereiches in Prozent.'; +$string['annotationareawidth_help'] = 'Die Breite des Annotationsbereichs in Prozent.'; $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; $string['toggleannotation'] = 'Annotation aus- / einklappen'; $string['toggleallannotations'] = 'Alle Annotation aus- / einklappen'; @@ -267,6 +255,21 @@ $string['hoverannotation'] = 'Annotation hervorheben'; $string['entryadded'] = 'Eintrag angelegt'; +// Admin settings. +$string['editall'] = 'Eigene Einträge bearbeiten'; +$string['editall_help'] = 'Wenn aktiviert können Lehrende in jedem Margic festlegen, ob Nutzer/innen ihre eigenen Einträge bearbeiten können.'; +$string['editdates'] = 'Eintragsdatum bearbeiten'; +$string['editdates_help'] = 'Wenn aktiviert können Lehrende in jedem Margic festlegen, ob Nutzer/innen das Datum jedes neuen Eintrags bearbeiten können.'; +$string['annotationareawidth'] = 'Breite des Annotationsbereichs'; +$string['annotationareawidthall'] = 'Die Breite des Annotationsbereichs in Prozent für alle Margics. Kann von Lehrenden in den einzelnen Margics überschrieben werden.'; +$string['editability'] = 'Bearbeitbarkeit'; +$string['entrybgc_title'] = 'Hintergrundfarbe für die Einträge und Annotationen'; +$string['entrybgc_descr'] = 'Hier kann die Hintergrundfarbe der Bereiche für die Einträge und Annotationen festgelegt werden.'; +$string['entrybgc_colour'] = '#C8E5FD'; +$string['textbgc_title'] = 'Hintergrundfarbe der Texte'; +$string['textbgc_descr'] = 'Hier kann die Hintergrundfarbe der Texte in den Einträgen und Annotationen festgelegt werden.'; +$string['textbgc_colour'] = '#F9F5F0'; + // Privacy. $string['privacy:metadata:margic_entries'] = 'Enthält die gespeicherten Benutzereinträge aller Margics.'; $string['privacy:metadata:margic_annotations'] = 'Enthält die in allen Margics gemacht Annotationen.'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 8b62a6b..82e82de 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -53,23 +53,13 @@ $string['margicentrydate'] = 'Set date for this entry'; $string['margicopentime'] = 'Open time'; $string['margicopentime_help'] = 'If enabled, you can set a date for the margic to be opened for use.'; -$string['editall'] = 'Edit own entries'; -$string['editall_help'] = 'When enabled, users can edit all own entries in the margic.'; -$string['editdates'] = 'Edit entry dates'; -$string['editdates_help'] = 'When enabled, users can edit the date of any entry.'; $string['editingstarts'] = 'Editing period starts at {$a}'; $string['editingended'] = 'Editing period has ended at {$a}'; $string['editingends'] = 'Editing period ends at {$a}'; $string['editthisentry'] = 'Edit this entry'; $string['entries'] = 'Entries'; $string['entry'] = 'Entry'; -$string['entrybgc_title'] = 'margic entry/feedback background color'; -$string['entrybgc_descr'] = 'This sets the background color of a margic entry/feedback.'; -$string['entrybgc_colour'] = '#C8E5FD'; $string['entrycomment'] = 'Entry comment'; -$string['entrytextbgc_title'] = 'margic text background color'; -$string['entrytextbgc_descr'] = 'This sets the background color of the text in a margic entry.'; -$string['entrytextbgc_colour'] = '#F9F5F0'; $string['exportfilenamemyentries'] = 'My_Margic_Entries'; $string['exportfilenamemargicentries'] = 'Margic_Entries'; $string['exportfilenameallentries'] = 'All_Margic_Entries'; @@ -231,8 +221,6 @@ $string['errfeedbacknotupdated'] = 'Feedback and grade not updated'; $string['errnograder'] = 'No grader.'; $string['errnofeedbackorratingdisabled'] = 'No feedback or rating disabled.'; -$string['annotationareawidth'] = 'Width of the annotation area'; -$string['annotationareawidthall'] = 'The width of the annotation area in percent for all margics. Can be overridden by teachers in the individual margics.'; $string['annotationareawidth_help'] = 'The width of the annotation area in percent.'; $string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; $string['toggleannotation'] = 'Toggle annotation'; @@ -280,6 +268,21 @@ $string['hoverannotation'] = 'Hover annotation'; $string['entryadded'] = 'Entry added'; +// Admin settings. +$string['editdates'] = 'Edit entry dates'; +$string['editdates_help'] = 'If enabled, teachers can configure in each margic whether users can edit their own entries.'; +$string['editall'] = 'Edit own entries'; +$string['editall_help'] = 'If enabled, teachers can configure in each margic whether users can edit the date of each new entry.'; +$string['annotationareawidth'] = 'Width of the annotation area'; +$string['annotationareawidthall'] = 'The width of the annotation area in percent for all margics. Can be overridden by teachers in the individual margics.'; +$string['editability'] = 'Editability'; +$string['entrybgc_title'] = 'Background color for the entries and annotations'; +$string['entrybgc_descr'] = 'Here you can set the background color of the areas for the entries and annotations.'; +$string['entrybgc_colour'] = '#C8E5FD'; +$string['textbgc_title'] = 'Background color of the texts'; +$string['textbgc_descr'] = 'Here you can set the background color of the texts in the entries and annotations.'; +$string['textbgc_colour'] = '#F9F5F0'; + // Privacy. $string['privacy:metadata:margic_entries'] = 'Contains the user entries saved in all margics.'; $string['privacy:metadata:margic_annotations'] = 'Contains the annotations made in all margics.'; diff --git a/locallib.php b/locallib.php index 54b3919..2aad4a7 100644 --- a/locallib.php +++ b/locallib.php @@ -333,7 +333,7 @@ public function get_annotationarea_width() { if (isset($this->instance->annotationareawidth)) { $annotationareawidth = $this->instance->annotationareawidth; } else { - $annotationareawidth = get_config('mod_margic', 'annotationareawidth'); + $annotationareawidth = get_config('margic', 'annotationareawidth'); } return $annotationareawidth; diff --git a/mod_form.php b/mod_form.php index fc3d3bd..2ea9836 100644 --- a/mod_form.php +++ b/mod_form.php @@ -21,6 +21,7 @@ * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/course/moodleform_mod.php'); @@ -54,9 +55,9 @@ public function definition() { $this->standard_intro_elements(get_string('margicdescription', 'margic')); - $id = optional_param('update', null, PARAM_INT); + $update = optional_param('update', null, PARAM_INT); - if (!isset($id) || $id == 0) { + if (!isset($update) || $update == 0) { // Add the header for the error types. $mform->addElement('header', 'errortypeshdr', get_string('errortypes', 'margic')); $mform->setExpanded('errortypeshdr'); @@ -91,24 +92,28 @@ public function definition() { $mform->addElement('header', 'availibilityhdr', get_string('availability')); $mform->addElement('date_time_selector', 'timeopen', get_string('margicopentime', 'margic'), array( - 'optional' => true, - 'step' => 1 + 'optional' => true )); $mform->addHelpButton('timeopen', 'margicopentime', 'margic'); $mform->addElement('date_time_selector', 'timeclose', get_string('margicclosetime', 'margic'), array( - 'optional' => true, - 'step' => 1 + 'optional' => true )); $mform->addHelpButton('timeclose', 'margicclosetime', 'margic'); // Edit all setting if user can edit its own entries. - $mform->addElement('selectyesno', 'editall', get_string('editall', 'margic')); - $mform->addHelpButton('editall', 'editall', 'margic'); + if (get_config('margic', 'editall')) { + $mform->addElement('selectyesno', 'editall', get_string('editall', 'margic')); + $mform->addHelpButton('editall', 'editall', 'margic'); + $mform->setDefault('editall', 1); + } // Edit dates setting if user can modify entry date. - $mform->addElement('selectyesno', 'editdates', get_string('editdates', 'margic')); - $mform->addHelpButton('editdates', 'editdates', 'margic'); + if (get_config('margic', 'editdates')) { + $mform->addElement('selectyesno', 'editdates', get_string('editdates', 'margic')); + $mform->addHelpButton('editdates', 'editdates', 'margic'); + $mform->setDefault('editdates', 0); + } // Add the header for appearance. $mform->addElement('header', 'appearancehdr', get_string('appearance')); @@ -117,7 +122,10 @@ public function definition() { $mform->addElement('text', 'annotationareawidth', get_string('annotationareawidth', 'margic')); $mform->setType('annotationareawidth', PARAM_INT); $mform->addHelpButton('annotationareawidth', 'annotationareawidth', 'margic'); - $mform->setDefault('annotationareawidth', get_config('mod_margic', 'annotationareawidth')); + + if (!isset($update) || $update == 0) { // If not updating existing instance set default to config value. + $mform->setDefault('annotationareawidth', get_config('mod_margic', 'annotationareawidth')); + } // Add the rest of the common settings. $this->standard_grading_coursemodule_elements(); diff --git a/settings.php b/settings.php index 10314c7..f6ab840 100644 --- a/settings.php +++ b/settings.php @@ -21,14 +21,15 @@ * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ + defined('MOODLE_INTERNAL') || die(); if ($ADMIN->fulltree) { - // Availability settings. - $settings->add(new admin_setting_heading('mod_margic/availibility', get_string('availability'), '')); + // Editability settings. + $settings->add(new admin_setting_heading('margic/editability', get_string('editability', 'margic'), '')); - // 20201015 Default edit all entries setting. + // Edit all own entries. $settings->add(new admin_setting_configselect('margic/editall', get_string('editall', 'margic'), get_string('editall_help', 'margic'), 1, array( @@ -36,7 +37,7 @@ '1' => get_string('yes') ))); - // 20201119 Default edit the date of any entry setting. + // Change the date of any new entry. $settings->add(new admin_setting_configselect('margic/editdates', get_string('editdates', 'margic'), get_string('editdates_help', 'margic'), 1, array( @@ -45,15 +46,14 @@ ))); // Appearance settings. - $settings->add(new admin_setting_heading('mod_margic/appearance', - get_string('appearance'), '')); + $settings->add(new admin_setting_heading('margic/appearance', get_string('appearance'), '')); // Default width of annotation area. - $settings->add(new admin_setting_configtext('mod_margic/annotationareawidth', get_string('annotationareawidth', 'margic'), + $settings->add(new admin_setting_configtext('margic/annotationareawidth', get_string('annotationareawidth', 'margic'), get_string('annotationareawidthall', 'margic'), 40, '/^([2-7]\d|80)+$/')); // Range allowed: 20-80 - // margic entry/feedback background colour setting. - $name = 'mod_margic/entrybgc'; + // Background color of entry and annotation area. + $name = 'margic/entrybgc'; $title = get_string('entrybgc_title', 'margic'); $description = get_string('entrybgc_descr', 'margic'); $default = get_string('entrybgc_colour', 'margic'); @@ -61,11 +61,11 @@ $setting->set_updatedcallback('theme_reset_all_caches'); $settings->add($setting); - // margic entry text background colour setting. - $name = 'mod_margic/entrytextbgc'; - $title = get_string('entrytextbgc_title', 'margic'); - $description = get_string('entrytextbgc_descr', 'margic'); - $default = get_string('entrytextbgc_colour', 'margic'); + // Background color of texts. + $name = 'margic/textbgc'; + $title = get_string('textbgc_title', 'margic'); + $description = get_string('textbgc_descr', 'margic'); + $default = get_string('textbgc_colour', 'margic'); $setting = new admin_setting_configcolourpicker($name, $title, $description, $default); $setting->set_updatedcallback('theme_reset_all_caches'); $settings->add($setting); diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index 74efbce..39eb0bb 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -40,7 +40,7 @@
{{#text}} -
+
{{{text}}}
{{/text}} @@ -58,7 +58,7 @@ {{#annotationmode}}
{{#annotations}} -
+
{{type}} @@ -89,7 +89,7 @@ {{/annotations}} {{#annotationform}} -
+
{{#str}}annotatedtextnotfound, mod_margic {{/str}}
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index be569e4..4972bd2 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -116,7 +116,7 @@ {{#text}} -
+
{{{text}}}
{{/text}} @@ -135,7 +135,7 @@ {{#annotationmode}}
{{#annotations}} -
+
{{type}} @@ -167,7 +167,7 @@ {{/annotations}} {{#annotationform}} -
+
{{#str}}annotatedtextnotfound, mod_margic {{/str}}
@@ -181,7 +181,7 @@ '; } - /* if (isset($activity->grade)) { - echo '
'; - - if ($CFG->branch > 310) { - echo get_string('gradenoun').': '; - } else { - echo get_string('grade').': '; - } - echo $activity->grade; - echo '
'; - } */ - echo ''; @@ -793,7 +781,7 @@ function margic_grade_item_update($margic, $grades = null) { 'idnumber' => $margic->cmidnumber ); - if (! $margic->assessed or $margic->scale == 0) { + if (! $margic->assessed || $margic->scale == 0) { $params['gradetype'] = GRADE_TYPE_NONE; } else if ($margic->scale > 0) { $params['gradetype'] = GRADE_TYPE_VALUE; diff --git a/locallib.php b/locallib.php index 3ba61e1..7854922 100644 --- a/locallib.php +++ b/locallib.php @@ -152,15 +152,19 @@ function sortannotation($a, $b) { if (has_capability('mod/margic:addentries', $context)) { switch ($action) { case 'currenttooldest': + require_sesskey(); set_user_preference('margic_sortoption', 1); break; case 'oldesttocurrent': + require_sesskey(); set_user_preference('margic_sortoption', 2); break; case 'lowestgradetohighest': + require_sesskey(); set_user_preference('margic_sortoption', 3); break; case 'highestgradetolowest': + require_sesskey(); set_user_preference('margic_sortoption', 4); break; default: @@ -196,6 +200,8 @@ function sortannotation($a, $b) { // Page selector. if ($pagecount !== 0) { + require_sesskey(); + if ($pagecount < 2) { $pagecount = 2; } diff --git a/settings.php b/settings.php index 069b095..daa9f53 100644 --- a/settings.php +++ b/settings.php @@ -50,7 +50,7 @@ // Default width of annotation area. $settings->add(new admin_setting_configtext('margic/annotationareawidth', get_string('annotationareawidth', 'margic'), - get_string('annotationareawidthall', 'margic'), 40, '/^([2-7]\d|80)+$/')); // Range allowed: 20-80 + get_string('annotationareawidthall', 'margic'), 40, '/^([2-7]\d|80)+$/')); // Range allowed: 20-80. // Background color of entry and annotation area. $name = 'margic/entrybgc'; diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index 44444b0..45b210a 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -76,14 +76,14 @@
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} + {{#exact}}{{exact}}{{/exact}}{{^exact}}{{#str}}annotatedtextnotfound, mod_margic {{/str}} {{/exact}}
{{#text}}{{text}}{{/text}} {{^text}}-{{/text}} {{#canbeedited}} - + {{/canbeedited}}
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index 20aa59a..fb771ca 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -94,14 +94,14 @@
- {{#str}}annotatedtextnotfound, mod_margic {{/str}} + {{#exact}}{{exact}}{{/exact}}{{^exact}}{{#str}}annotatedtextnotfound, mod_margic {{/str}} {{/exact}}
{{#text}}{{text}}{{/text}} {{^text}}-{{/text}} {{#canbeedited}} - + {{/canbeedited}}
diff --git a/templates/margic_error_summary.mustache b/templates/margic_error_summary.mustache index 888396b..97d751b 100644 --- a/templates/margic_error_summary.mustache +++ b/templates/margic_error_summary.mustache @@ -39,10 +39,10 @@
{{#canbeedited}} - + {{/canbeedited}} - - + +
{{/margicerrortypes}} @@ -98,10 +98,10 @@ {{#canbeedited}}{{/canbeedited}} - {{#canbeedited}}{{/canbeedited}} + {{#canbeedited}}{{/canbeedited}} - + {{/errortypetemplates}} diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index 92e3331..cab79dc 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -57,12 +57,12 @@ {{#canmanageentries}} {{#str}}errorsummary, mod_margic{{/str}} {{/canmanageentries}} {{#entries.0}} - - - + + + {{#ratingaggregationmode}} - - + + {{/ratingaggregationmode}} {{/entries.0}}
diff --git a/view.php b/view.php index d58d43a..13cb7da 100644 --- a/view.php +++ b/view.php @@ -92,6 +92,8 @@ // Toolbar action handler for download. if (!empty($action) && $action == 'download' && has_capability('mod/margic:addentries', $context)) { + require_sesskey(); + // Call download entries function in lib.php. helper::download_entries($context, $course, $moduleinstance); From 4e703a80d093e9dc5b9f866240862925162ed449 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Thu, 8 Sep 2022 19:58:56 +0200 Subject: [PATCH 58/60] fix(coding style): multiple coding style changes --- README.md | 2 +- amd/build/highlighting.min.js.map | 2 +- amd/build/html.min.js | 3 -- amd/build/html.min.js.map | 1 - amd/build/match-quote.min.js.map | 2 +- amd/build/string-match.min.js.map | 2 +- amd/build/text-range.min.js | 2 +- amd/build/text-range.min.js.map | 2 +- amd/build/types.min.js.map | 2 +- amd/src/highlighting.js | 6 ++-- amd/src/match-quote.js | 8 +++++ amd/src/string-match.js | 2 +- amd/src/text-range.js | 12 +++++--- amd/src/types.js | 8 ++--- annotations.php | 4 +-- classes/local/helper.php | 12 +++----- classes/output/margic_error_summary.php | 4 +-- classes/privacy/provider.php | 2 -- db/access.php | 13 +++++++- db/install.php | 1 + db/install.xml | 8 ++--- edit.php | 2 +- error_summary.php | 2 +- errortypes.php | 2 +- grade_entry.php | 2 +- index.php | 2 +- lang/de/margic.php | 41 +++++++++++++------------ lang/en/margic.php | 38 ++++++++++++----------- lib.php | 1 + locallib.php | 2 -- mod_form.php | 2 +- pix/icon.svg | 2 +- styles.css | 6 ++-- templates/margic_childentry.mustache | 1 + templates/margic_entry.mustache | 7 ----- templates/margic_error_summary.mustache | 2 +- version.php | 6 ++-- 37 files changed, 112 insertions(+), 104 deletions(-) delete mode 100644 amd/build/html.min.js delete mode 100644 amd/build/html.min.js.map diff --git a/README.md b/README.md index efd4dba..ee74e8c 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Core features of the plugin: - Write and revise multimedia entries. - Individually customizable overview page with all (own) entries available in Margic - Extensive possibilities for annotation and evaluation of entries for teachers -- Customizable error types and accurate error evaluation +- Customizable error types and detailed error evaluation ## Quick installation instructions ## diff --git a/amd/build/highlighting.min.js.map b/amd/build/highlighting.min.js.map index 448e42a..61b78cf 100644 --- a/amd/build/highlighting.min.js.map +++ b/amd/build/highlighting.min.js.map @@ -1 +1 @@ -{"version":3,"file":"highlighting.min.js","sources":["../src/highlighting.js"],"sourcesContent":["/**\n * Functions for the highlighting and anchoring of annotations.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport $ from 'jquery';\nimport {RangeAnchor, TextPositionAnchor, TextQuoteAnchor} from './types';\nimport {TextRange} from './text-range';\n\n/**\n * Get anchors for new annnotation.\n *\n * @param {Element} root\n * @param {Range} range\n * @return {object} - Array with the anchors.\n */\nexport function describe(root, range) {\n const types = [RangeAnchor, TextPositionAnchor, TextQuoteAnchor];\n const result = [];\n\n for (let type of types) {\n try {\n const anchor = type.fromRange(root, range);\n\n result.push(anchor.toSelector());\n } catch (error) {\n continue;\n }\n }\n return result;\n}\n\n/**\n * Anchor an annotation's selectors in the document.\n *\n * _Anchoring_ resolves a set of selectors to a concrete region of the document\n * which is then highlighted.\n *\n * Any existing anchors associated with `annotation` will be removed before\n * re-anchoring the annotation.\n *\n * @param {AnnotationData} annotation\n * @param {obj} root\n * @return {obj} achor object\n */\n export function anchor(annotation, root) {\n /**\n * Resolve an annotation's selectors to a concrete range.\n *\n * @param {Target} target\n * @return {obj}\n */\n const locate = target => {\n\n // Only annotations with an associated quote can currently be anchored.\n // This is because the quote is used to verify anchoring with other selector\n // types.\n if (\n !target.selector ||\n !target.selector.some(s => s.type === 'TextQuoteSelector')\n ) {\n return {annotation, target};\n }\n\n /** @type {Anchor} */\n let anchor;\n try {\n const range = htmlAnchor(root, target.selector);\n // Convert the `Range` to a `TextRange` which can be converted back to\n // a `Range` later. The `TextRange` representation allows for highlights\n // to be inserted during anchoring other annotations without \"breaking\"\n // this anchor.\n\n\n const textRange = TextRange.fromRange(range);\n\n anchor = { annotation, target, range: textRange };\n\n } catch (err) {\n\n anchor = { annotation, target };\n }\n\n return anchor;\n };\n\n /**\n * Highlight the text range that `anchor` refers to.\n *\n * @param {Anchor} anchor\n */\n const highlight = anchor => {\n\n const range = resolveAnchor(anchor);\n\n if (!range) {\n return;\n }\n\n let highlights = [];\n\n if (annotation.annotation) {\n highlights = highlightRange(range, annotation.annotation.id, 'annotated', annotation.annotation.color);\n } else {\n highlights = highlightRange(range, false, 'annotated_temp');\n }\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n };\n\n // Remove existing anchors for this annotation.\n // this.detach(annotation, false /* notify */); // To be replaced by own method\n\n // Resolve selectors to ranges and insert highlights.\n if (!annotation.target) {\n annotation.target = [];\n }\n const anchors = annotation.target.map(locate);\n\n for (let anchor of anchors) {\n\n highlight(anchor);\n }\n\n // Set flag indicating whether anchoring succeeded. For each target,\n // anchoring is successful either if there are no selectors (ie. this is a\n // Page Note) or we successfully resolved the selectors to a range.\n annotation.$orphan =\n anchors.length > 0 &&\n anchors.every(anchor => anchor.target.selector && !anchor.range);\n\n return anchors;\n}\n\n/**\n * Resolve an anchor's associated document region to a concrete `Range`.\n *\n * This may fail if anchoring failed or if the document has been mutated since\n * the anchor was created in a way that invalidates the anchor.\n *\n * @param {Anchor} anchor\n * @return {Range|null}\n */\nfunction resolveAnchor(anchor) {\n\n if (!anchor.range) {\n return null;\n }\n try {\n return anchor.range.toRange();\n } catch {\n return null;\n }\n}\n\n/**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * Modified for handling annotations.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n const highlights = /** @type {HighlightElement[]} */ ([]);\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('margic-highlight');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n const parent = /** @type {Node} */ (nodes[0].parentNode);\n parent.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n highlights.push(highlightEl);\n\n });\n\n return highlights;\n}\n\n/**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n}\n\n/**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\nfunction isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n}\n\n/**\n * @param {RangeAnchor|TextPositionAnchor|TextQuoteAnchor} anchor\n * @param {Object} [options]\n * @return {obj} - range\n */\n function querySelector(anchor, options = {}) {\n\n return anchor.toRange(options);\n}\n\n/**\n * Anchor a set of selectors.\n *\n * This function converts a set of selectors into a document range.\n * It encapsulates the core anchoring algorithm, using the selectors alone or\n * in combination to establish the best anchor within the document.\n *\n * @param {Element} root - The root element of the anchoring context.\n * @param {Selector[]} selectors - The selectors to try.\n * @param {Object} [options]\n * @return {object} the query selector\n */\n function htmlAnchor(root, selectors, options = {}) {\n let position = null;\n let quote = null;\n let range = null;\n\n // Collect all the selectors\n for (let selector of selectors) {\n switch (selector.type) {\n case 'TextPositionSelector':\n position = selector;\n options.hint = position.start; // TextQuoteAnchor hint\n break;\n case 'TextQuoteSelector':\n quote = selector;\n break;\n case 'RangeSelector':\n range = selector;\n break;\n }\n }\n\n /**\n * Assert the quote matches the stored quote, if applicable\n * @param {Range} range\n * @return {Range} range\n */\n const maybeAssertQuote = range => {\n\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n return queryselector;\n }\n } catch (error) {\n return false;\n }\n }\n }\n}\n\n/**\n * Remove all temporary highlights under a given root element.\n */\n export function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n}\n\n/**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n //var pn = highlights[i].parentNode;\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n //pn.normalize(); // To Be removed?\n }\n }\n}\n\n/**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\nfunction replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n}"],"names":["highlightRange","range","annotationid","cssClass","color","textNodes","wholeTextNodesInRange","textNodeSpans","prevNode","currentSpan","forEach","node","nextSibling","push","whitespace","filter","span","some","test","nodeValue","highlights","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","parentNode","replaceChild","appendChild","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","text","startContainer","startOffset","splitText","endContainer","endOffset","data","length","_node$nodeValue","childNodes","comparePoint","e","querySelector","anchor","options","toRange","replaceWith","replacements","parent","r","insertBefore","remove","annotation","highlight","resolveAnchor","h","_annotation","target","anchors","map","selector","s","type","selectors","position","quote","hint","start","maybeAssertQuote","exact","toString","Error","RangeAnchor","fromSelector","error","TextPositionAnchor","TextQuoteAnchor","htmlAnchor","textRange","TextRange","fromRange","err","$orphan","every","types","result","toSelector","Array","from","querySelectorAll","undefined","i","children","removeHighlights"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"highlighting.min.js","sources":["../src/highlighting.js"],"sourcesContent":["/**\n * Functions for the highlighting and anchoring of annotations.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport $ from 'jquery';\nimport {RangeAnchor, TextPositionAnchor, TextQuoteAnchor} from './types';\nimport {TextRange} from './text-range';\n\n/**\n * Get anchors for new annnotation.\n *\n * @param {Element} root\n * @param {Range} range\n * @return {object} - Array with the anchors.\n */\nexport function describe(root, range) {\n const types = [RangeAnchor, TextPositionAnchor, TextQuoteAnchor];\n const result = [];\n\n for (let type of types) {\n try {\n const anchor = type.fromRange(root, range);\n\n result.push(anchor.toSelector());\n } catch (error) {\n continue;\n }\n }\n return result;\n}\n\n/**\n * Anchor an annotation's selectors in the document.\n *\n * _Anchoring_ resolves a set of selectors to a concrete region of the document\n * which is then highlighted.\n *\n * Any existing anchors associated with `annotation` will be removed before\n * re-anchoring the annotation.\n *\n * @param {AnnotationData} annotation\n * @param {obj} root\n * @return {obj} achor object\n */\n export function anchor(annotation, root) {\n /**\n * Resolve an annotation's selectors to a concrete range.\n *\n * @param {Target} target\n * @return {obj}\n */\n const locate = target => {\n\n // Only annotations with an associated quote can currently be anchored.\n // This is because the quote is used to verify anchoring with other selector\n // types.\n if (\n !target.selector ||\n !target.selector.some(s => s.type === 'TextQuoteSelector')\n ) {\n return {annotation, target};\n }\n\n /** @type {Anchor} */\n let anchor;\n try {\n const range = htmlAnchor(root, target.selector);\n // Convert the `Range` to a `TextRange` which can be converted back to\n // a `Range` later. The `TextRange` representation allows for highlights\n // to be inserted during anchoring other annotations without \"breaking\"\n // this anchor.\n\n\n const textRange = TextRange.fromRange(range);\n\n anchor = {annotation, target, range: textRange};\n\n } catch (err) {\n\n anchor = {annotation, target};\n }\n\n return anchor;\n };\n\n /**\n * Highlight the text range that `anchor` refers to.\n *\n * @param {Anchor} anchor\n */\n const highlight = anchor => {\n\n const range = resolveAnchor(anchor);\n\n if (!range) {\n return;\n }\n\n let highlights = [];\n\n if (annotation.annotation) {\n highlights = highlightRange(range, annotation.annotation.id, 'annotated', annotation.annotation.color);\n } else {\n highlights = highlightRange(range, false, 'annotated_temp');\n }\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n };\n\n // Remove existing anchors for this annotation.\n // this.detach(annotation, false /* notify */); // To be replaced by own method\n\n // Resolve selectors to ranges and insert highlights.\n if (!annotation.target) {\n annotation.target = [];\n }\n const anchors = annotation.target.map(locate);\n\n for (let anchor of anchors) {\n\n highlight(anchor);\n }\n\n // Set flag indicating whether anchoring succeeded. For each target,\n // anchoring is successful either if there are no selectors (ie. this is a\n // Page Note) or we successfully resolved the selectors to a range.\n annotation.$orphan =\n anchors.length > 0 &&\n anchors.every(anchor => anchor.target.selector && !anchor.range);\n\n return anchors;\n}\n\n/**\n * Resolve an anchor's associated document region to a concrete `Range`.\n *\n * This may fail if anchoring failed or if the document has been mutated since\n * the anchor was created in a way that invalidates the anchor.\n *\n * @param {Anchor} anchor\n * @return {Range|null}\n */\nfunction resolveAnchor(anchor) {\n\n if (!anchor.range) {\n return null;\n }\n try {\n return anchor.range.toRange();\n } catch {\n return null;\n }\n}\n\n/**\n * Wraps the DOM Nodes within the provided range with a highlight\n * element of the specified class and returns the highlight Elements.\n *\n * Modified for handling annotations.\n *\n * @param {Range} range - Range to be highlighted\n * @param {int} annotationid - ID of annotation\n * @param {string} cssClass - A CSS class to use for the highlight\n * @param {string} color - Color of the highlighting\n * @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect\n */\n function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {\n\n const textNodes = wholeTextNodesInRange(range);\n\n // Group text nodes into spans of adjacent nodes. If a group of text nodes are\n // adjacent, we only need to create one highlight element for the group.\n let textNodeSpans = [];\n let prevNode = null;\n let currentSpan = null;\n\n textNodes.forEach(node => {\n if (prevNode && prevNode.nextSibling === node) {\n currentSpan.push(node);\n } else {\n currentSpan = [node];\n textNodeSpans.push(currentSpan);\n }\n prevNode = node;\n });\n\n // Filter out text node spans that consist only of white space. This avoids\n // inserting highlight elements in places that can only contain a restricted\n // subset of nodes such as table rows and lists.\n const whitespace = /^\\s*$/;\n textNodeSpans = textNodeSpans.filter(span =>\n // Check for at least one text node with non-space content.\n span.some(node => !whitespace.test(node.nodeValue))\n );\n\n // Wrap each text node span with a `` element.\n const highlights = /** @type {HighlightElement[]} */ ([]);\n\n textNodeSpans.forEach(nodes => {\n const highlightEl = document.createElement('margic-highlight');\n highlightEl.className = cssClass;\n\n if (annotationid) {\n highlightEl.className += ' ' + cssClass + '-' + annotationid;\n highlightEl.style = \"text-decoration:underline; text-decoration-color: #\" + color;\n highlightEl.id = cssClass + '-' + annotationid;\n highlightEl.style.backgroundColor = '#' + color;\n }\n\n const parent = /** @type {Node} */ (nodes[0].parentNode);\n parent.replaceChild(highlightEl, nodes[0]);\n nodes.forEach(node => highlightEl.appendChild(node));\n\n highlights.push(highlightEl);\n\n });\n\n return highlights;\n}\n\n/**\n * Return text nodes which are entirely inside `range`.\n *\n * If a range starts or ends part-way through a text node, the node is split\n * and the part inside the range is returned.\n *\n * @param {Range} range\n * @return {Text[]}\n */\n function wholeTextNodesInRange(range) {\n if (range.collapsed) {\n // Exit early for an empty range to avoid an edge case that breaks the algorithm\n // below. Splitting a text node at the start of an empty range can leave the\n // range ending in the left part rather than the right part.\n return [];\n }\n\n /** @type {Node|null} */\n let root = range.commonAncestorContainer;\n if (root.nodeType !== Node.ELEMENT_NODE) {\n // If the common ancestor is not an element, set it to the parent element to\n // ensure that the loop below visits any text nodes generated by splitting\n // the common ancestor.\n //\n // Note that `parentElement` may be `null`.\n root = root.parentElement;\n }\n\n if (!root) {\n // If there is no root element then we won't be able to insert highlights,\n // so exit here.\n return [];\n }\n\n const textNodes = [];\n const nodeIter = /** @type {Document} */ (\n root.ownerDocument\n ).createNodeIterator(\n root,\n NodeFilter.SHOW_TEXT // Only return `Text` nodes.\n );\n let node;\n while ((node = nodeIter.nextNode())) {\n if (!isNodeInRange(range, node)) {\n continue;\n }\n let text = /** @type {Text} */ (node);\n\n if (text === range.startContainer && range.startOffset > 0) {\n // Split `text` where the range starts. The split will create a new `Text`\n // node which will be in the range and will be visited in the next loop iteration.\n text.splitText(range.startOffset);\n continue;\n }\n\n if (text === range.endContainer && range.endOffset < text.data.length) {\n // Split `text` where the range ends, leaving it as the part in the range.\n text.splitText(range.endOffset);\n }\n\n textNodes.push(text);\n }\n\n return textNodes;\n}\n\n/**\n * Returns true if any part of `node` lies within `range`.\n *\n * @param {Range} range\n * @param {Node} node\n * @return {bool} - If node is in range\n */\nfunction isNodeInRange(range, node) {\n try {\n const length = node.nodeValue?.length ?? node.childNodes.length;\n return (\n // Check start of node is before end of range.\n range.comparePoint(node, 0) <= 0 &&\n // Check end of node is after start of range.\n range.comparePoint(node, length) >= 0\n );\n } catch (e) {\n // `comparePoint` may fail if the `range` and `node` do not share a common\n // ancestor or `node` is a doctype.\n return false;\n }\n}\n\n/**\n * @param {RangeAnchor|TextPositionAnchor|TextQuoteAnchor} anchor\n * @param {Object} [options]\n * @return {obj} - range\n */\n function querySelector(anchor, options = {}) {\n\n return anchor.toRange(options);\n}\n\n/**\n * Anchor a set of selectors.\n *\n * This function converts a set of selectors into a document range.\n * It encapsulates the core anchoring algorithm, using the selectors alone or\n * in combination to establish the best anchor within the document.\n *\n * @param {Element} root - The root element of the anchoring context.\n * @param {Selector[]} selectors - The selectors to try.\n * @param {Object} [options]\n * @return {object} the query selector\n */\n function htmlAnchor(root, selectors, options = {}) {\n let position = null;\n let quote = null;\n let range = null;\n\n // Collect all the selectors\n for (let selector of selectors) {\n switch (selector.type) {\n case 'TextPositionSelector':\n position = selector;\n options.hint = position.start; // TextQuoteAnchor hint\n break;\n case 'TextQuoteSelector':\n quote = selector;\n break;\n case 'RangeSelector':\n range = selector;\n break;\n }\n }\n\n /**\n * Assert the quote matches the stored quote, if applicable\n * @param {Range} range\n * @return {Range} range\n */\n const maybeAssertQuote = range => {\n\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n return queryselector;\n }\n } catch (error) {\n return false;\n }\n }\n }\n}\n\n/**\n * Remove all temporary highlights under a given root element.\n */\n export function removeAllTempHighlights() {\n const highlights = Array.from($('body')[0].querySelectorAll('.annotated_temp'));\n if (highlights !== undefined && highlights.length != 0) {\n removeHighlights(highlights);\n }\n}\n\n/**\n * Remove highlights from a range previously highlighted with `highlightRange`.\n *\n * @param {HighlightElement[]} highlights - The highlight elements returned by `highlightRange`\n */\n function removeHighlights(highlights) {\n\n for (var i = 0; i < highlights.length; i++) {\n if (highlights[i].parentNode) {\n const children = Array.from(highlights[i].childNodes);\n replaceWith(highlights[i], children);\n }\n }\n}\n\n/**\n * Replace a child `node` with `replacements`.\n *\n * nb. This is like `ChildNode.replaceWith` but it works in older browsers.\n *\n * @param {ChildNode} node\n * @param {Node[]} replacements\n */\nfunction replaceWith(node, replacements) {\n const parent = /** @type {Node} */ (node.parentNode);\n\n replacements.forEach(r => parent.insertBefore(r, node));\n node.remove();\n}"],"names":["highlightRange","range","annotationid","cssClass","color","textNodes","wholeTextNodesInRange","textNodeSpans","prevNode","currentSpan","forEach","node","nextSibling","push","whitespace","filter","span","some","test","nodeValue","highlights","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","parentNode","replaceChild","appendChild","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","nextNode","isNodeInRange","text","startContainer","startOffset","splitText","endContainer","endOffset","data","length","_node$nodeValue","childNodes","comparePoint","e","querySelector","anchor","options","toRange","replaceWith","replacements","parent","r","insertBefore","remove","annotation","highlight","resolveAnchor","h","_annotation","target","anchors","map","selector","s","type","selectors","position","quote","hint","start","maybeAssertQuote","exact","toString","Error","RangeAnchor","fromSelector","error","TextPositionAnchor","TextQuoteAnchor","htmlAnchor","textRange","TextRange","fromRange","err","$orphan","every","types","result","toSelector","Array","from","querySelectorAll","undefined","i","children","removeHighlights"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/html.min.js b/amd/build/html.min.js deleted file mode 100644 index 18a69f5..0000000 --- a/amd/build/html.min.js +++ /dev/null @@ -1,3 +0,0 @@ -define("mod_margic/html",["./types"],(function(_types){})); - -//# sourceMappingURL=html.min.js.map \ No newline at end of file diff --git a/amd/build/html.min.js.map b/amd/build/html.min.js.map deleted file mode 100644 index 95bcfcb..0000000 --- a/amd/build/html.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"html.min.js","sources":[],"sourcesContent":[],"names":[],"mappings":""} \ No newline at end of file diff --git a/amd/build/match-quote.min.js.map b/amd/build/match-quote.min.js.map index ee7ff90..e122f88 100644 --- a/amd/build/match-quote.min.js.map +++ b/amd/build/match-quote.min.js.map @@ -1 +1 @@ -{"version":3,"file":"match-quote.min.js","sources":["../src/match-quote.js"],"sourcesContent":["import approxSearch from './string-match';\n\n/**\n * @typedef {import('approx-string-match').Match} StringMatch\n */\n\n/**\n * @typedef Match\n * @prop {number} start - Start offset of match in text\n * @prop {number} end - End offset of match in text\n * @prop {number} score -\n * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match\n * for the quote and context.\n */\n\n/**\n * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors.\n *\n * @param {string} text\n * @param {string} str\n * @param {number} maxErrors\n * @return {StringMatch[]}\n */\nfunction search(text, str, maxErrors) {\n // Do a fast search for exact matches. The `approx-string-match` library\n // doesn't currently incorporate this optimization itself.\n let matchPos = 0;\n let exactMatches = [];\n while (matchPos !== -1) {\n matchPos = text.indexOf(str, matchPos);\n if (matchPos !== -1) {\n exactMatches.push({\n start: matchPos,\n end: matchPos + str.length,\n errors: 0,\n });\n matchPos += 1;\n }\n }\n if (exactMatches.length > 0) {\n return exactMatches;\n }\n\n // If there are no exact matches, do a more expensive search for matches\n // with errors.\n return approxSearch(text, str, maxErrors);\n}\n\n/**\n * Compute a score between 0 and 1.0 for the similarity between `text` and `str`.\n *\n * @param {string} text\n * @param {string} str\n * @return {int}\n */\nfunction textMatchScore(text, str) {\n // `search` will return no matches if either the text or pattern is empty,\n // otherwise it will return at least one match if the max allowed error count\n // is at least `str.length`.\n if (str.length === 0 || text.length === 0) {\n return 0.0;\n }\n\n const matches = search(text, str, str.length);\n\n // Prettier-ignore.\n return 1 - (matches[0].errors / str.length);\n}\n\n/**\n * Find the best approximate match for `quote` in `text`.\n *\n * Returns `null` if no match exceeding the minimum quality threshold was found.\n *\n * @param {string} text - Document text to search\n * @param {string} quote - String to find within `text`\n * @param {Object} context -\n * Context in which the quote originally appeared. This is used to choose the\n * best match.\n * @param {string} [context.prefix] - Expected text before the quote\n * @param {string} [context.suffix] - Expected text after the quote\n * @param {number} [context.hint] - Expected offset of match within text\n * @return {Match|null}\n */\nexport function matchQuote(text, quote, context = {}) {\n if (quote.length === 0) {\n return null;\n }\n\n // Choose the maximum number of errors to allow for the initial search.\n // This choice involves a tradeoff between:\n //\n // - Recall (proportion of \"good\" matches found)\n // - Precision (proportion of matches found which are \"good\")\n // - Cost of the initial search and of processing the candidate matches [1]\n //\n // [1] Specifically, the expected-time complexity of the initial search is\n // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs.\n const maxErrors = Math.min(256, quote.length / 2);\n\n // Find closest matches for `quote` in `text` based on edit distance.\n const matches = search(text, quote, maxErrors);\n\n if (matches.length === 0) {\n return null;\n }\n\n /**\n * Compute a score between 0 and 1.0 for a match candidate.\n *\n * @param {StringMatch} match\n * @return {int}\n */\n const scoreMatch = match => {\n const quoteWeight = 50; // Similarity of matched text to quote.\n const prefixWeight = 20; // Similarity of text before matched text to `context.prefix`.\n const suffixWeight = 20; // Similarity of text after matched text to `context.suffix`.\n const posWeight = 2; // Proximity to expected location. Used as a tie-breaker.\n\n const quoteScore = 1 - match.errors / quote.length;\n\n const prefixScore = context.prefix\n ? textMatchScore(\n text.slice(\n Math.max(0, match.start - context.prefix.length),\n match.start\n ),\n context.prefix\n )\n : 1.0;\n const suffixScore = context.suffix\n ? textMatchScore(\n text.slice(match.end, match.end + context.suffix.length),\n context.suffix\n )\n : 1.0;\n\n let posScore = 1.0;\n if (typeof context.hint === 'number') {\n const offset = Math.abs(match.start - context.hint);\n posScore = 1.0 - offset / text.length;\n }\n\n const rawScore =\n quoteWeight * quoteScore +\n prefixWeight * prefixScore +\n suffixWeight * suffixScore +\n posWeight * posScore;\n const maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight;\n const normalizedScore = rawScore / maxScore;\n\n return normalizedScore;\n };\n\n // Rank matches based on similarity of actual and expected surrounding text\n // and actual/expected offset in the document text.\n const scoredMatches = matches.map(m => ({\n start: m.start,\n end: m.end,\n score: scoreMatch(m),\n }));\n\n // Choose match with highest score.\n scoredMatches.sort((a, b) => b.score - a.score);\n return scoredMatches[0];\n}\n"],"names":["search","text","str","maxErrors","matchPos","exactMatches","indexOf","push","start","end","length","errors","textMatchScore","quote","context","Math","min","matches","scoreMatch","match","quoteScore","prefixScore","prefix","slice","max","suffixScore","suffix","posScore","hint","abs","quoteWeight","scoredMatches","map","m","score","sort","a","b"],"mappings":"+GAuBSA,OAAOC,KAAMC,IAAKC,mBAGrBC,SAAW,EACXC,aAAe,IACE,IAAdD,WAEa,KADlBA,SAAWH,KAAKK,QAAQJ,IAAKE,aAE3BC,aAAaE,KAAK,CAChBC,MAAOJ,SACPK,IAAKL,SAAWF,IAAIQ,OACpBC,OAAQ,IAEVP,UAAY,UAGZC,aAAaK,OAAS,EACjBL,cAKF,wBAAaJ,KAAMC,IAAKC,oBAUxBS,eAAeX,KAAMC,YAIT,IAAfA,IAAIQ,QAAgC,IAAhBT,KAAKS,OACpB,EAMF,EAHSV,OAAOC,KAAMC,IAAKA,IAAIQ,QAGlB,GAAGC,OAAST,IAAIQ,4FAkBXT,KAAMY,WAAOC,+DAAU,MAC3B,IAAjBD,MAAMH,cACD,SAYHP,UAAYY,KAAKC,IAAI,IAAKH,MAAMH,OAAS,GAGzCO,QAAUjB,OAAOC,KAAMY,MAAOV,cAEb,IAAnBc,QAAQP,cACH,SASHQ,WAAa,SAAAC,WAMXC,WAAa,EAAID,MAAMR,OAASE,MAAMH,OAEtCW,YAAcP,QAAQQ,OACxBV,eACEX,KAAKsB,MACHR,KAAKS,IAAI,EAAGL,MAAMX,MAAQM,QAAQQ,OAAOZ,QACzCS,MAAMX,OAERM,QAAQQ,QAEV,EACEG,YAAcX,QAAQY,OACxBd,eACEX,KAAKsB,MAAMJ,MAAMV,IAAKU,MAAMV,IAAMK,QAAQY,OAAOhB,QACjDI,QAAQY,QAEV,EAEAC,SAAW,EACa,iBAAjBb,QAAQc,OAEjBD,SAAW,EADIZ,KAAKc,IAAIV,MAAMX,MAAQM,QAAQc,MACpB3B,KAAKS,eA1Bb,GA8BJU,WA7BK,GA8BJC,YA7BI,GA8BJI,YA7BC,EA8BJE,UACGG,IAQbC,cAAgBd,QAAQe,KAAI,SAAAC,SAAM,CACtCzB,MAAOyB,EAAEzB,MACTC,IAAKwB,EAAExB,IACPyB,MAAOhB,WAAWe,cAIpBF,cAAcI,MAAK,SAACC,EAAGC,UAAMA,EAAEH,MAAQE,EAAEF,SAClCH,cAAc"} \ No newline at end of file +{"version":3,"file":"match-quote.min.js","sources":["../src/match-quote.js"],"sourcesContent":["/**\n * Functions for quote matching for the annotations and highlighting.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport approxSearch from './string-match';\n\n/**\n * @typedef {import('approx-string-match').Match} StringMatch\n */\n\n/**\n * @typedef Match\n * @prop {number} start - Start offset of match in text\n * @prop {number} end - End offset of match in text\n * @prop {number} score -\n * Score for the match between 0 and 1.0, where 1.0 indicates a perfect match\n * for the quote and context.\n */\n\n/**\n * Find the best approximate matches for `str` in `text` allowing up to `maxErrors` errors.\n *\n * @param {string} text\n * @param {string} str\n * @param {number} maxErrors\n * @return {StringMatch[]}\n */\nfunction search(text, str, maxErrors) {\n // Do a fast search for exact matches. The `approx-string-match` library\n // doesn't currently incorporate this optimization itself.\n let matchPos = 0;\n let exactMatches = [];\n while (matchPos !== -1) {\n matchPos = text.indexOf(str, matchPos);\n if (matchPos !== -1) {\n exactMatches.push({\n start: matchPos,\n end: matchPos + str.length,\n errors: 0,\n });\n matchPos += 1;\n }\n }\n if (exactMatches.length > 0) {\n return exactMatches;\n }\n\n // If there are no exact matches, do a more expensive search for matches\n // with errors.\n return approxSearch(text, str, maxErrors);\n}\n\n/**\n * Compute a score between 0 and 1.0 for the similarity between `text` and `str`.\n *\n * @param {string} text\n * @param {string} str\n * @return {int}\n */\nfunction textMatchScore(text, str) {\n // `search` will return no matches if either the text or pattern is empty,\n // otherwise it will return at least one match if the max allowed error count\n // is at least `str.length`.\n if (str.length === 0 || text.length === 0) {\n return 0.0;\n }\n\n const matches = search(text, str, str.length);\n\n // Prettier-ignore.\n return 1 - (matches[0].errors / str.length);\n}\n\n/**\n * Find the best approximate match for `quote` in `text`.\n *\n * Returns `null` if no match exceeding the minimum quality threshold was found.\n *\n * @param {string} text - Document text to search\n * @param {string} quote - String to find within `text`\n * @param {Object} context -\n * Context in which the quote originally appeared. This is used to choose the\n * best match.\n * @param {string} [context.prefix] - Expected text before the quote\n * @param {string} [context.suffix] - Expected text after the quote\n * @param {number} [context.hint] - Expected offset of match within text\n * @return {Match|null}\n */\nexport function matchQuote(text, quote, context = {}) {\n if (quote.length === 0) {\n return null;\n }\n\n // Choose the maximum number of errors to allow for the initial search.\n // This choice involves a tradeoff between:\n //\n // - Recall (proportion of \"good\" matches found)\n // - Precision (proportion of matches found which are \"good\")\n // - Cost of the initial search and of processing the candidate matches [1]\n //\n // [1] Specifically, the expected-time complexity of the initial search is\n // `O((maxErrors / 32) * text.length)`. See `approx-string-match` docs.\n const maxErrors = Math.min(256, quote.length / 2);\n\n // Find closest matches for `quote` in `text` based on edit distance.\n const matches = search(text, quote, maxErrors);\n\n if (matches.length === 0) {\n return null;\n }\n\n /**\n * Compute a score between 0 and 1.0 for a match candidate.\n *\n * @param {StringMatch} match\n * @return {int}\n */\n const scoreMatch = match => {\n const quoteWeight = 50; // Similarity of matched text to quote.\n const prefixWeight = 20; // Similarity of text before matched text to `context.prefix`.\n const suffixWeight = 20; // Similarity of text after matched text to `context.suffix`.\n const posWeight = 2; // Proximity to expected location. Used as a tie-breaker.\n\n const quoteScore = 1 - match.errors / quote.length;\n\n const prefixScore = context.prefix\n ? textMatchScore(\n text.slice(\n Math.max(0, match.start - context.prefix.length),\n match.start\n ),\n context.prefix\n )\n : 1.0;\n const suffixScore = context.suffix\n ? textMatchScore(\n text.slice(match.end, match.end + context.suffix.length),\n context.suffix\n )\n : 1.0;\n\n let posScore = 1.0;\n if (typeof context.hint === 'number') {\n const offset = Math.abs(match.start - context.hint);\n posScore = 1.0 - offset / text.length;\n }\n\n const rawScore =\n quoteWeight * quoteScore +\n prefixWeight * prefixScore +\n suffixWeight * suffixScore +\n posWeight * posScore;\n const maxScore = quoteWeight + prefixWeight + suffixWeight + posWeight;\n const normalizedScore = rawScore / maxScore;\n\n return normalizedScore;\n };\n\n // Rank matches based on similarity of actual and expected surrounding text\n // and actual/expected offset in the document text.\n const scoredMatches = matches.map(m => ({\n start: m.start,\n end: m.end,\n score: scoreMatch(m),\n }));\n\n // Choose match with highest score.\n scoredMatches.sort((a, b) => b.score - a.score);\n return scoredMatches[0];\n}\n"],"names":["search","text","str","maxErrors","matchPos","exactMatches","indexOf","push","start","end","length","errors","textMatchScore","quote","context","Math","min","matches","scoreMatch","match","quoteScore","prefixScore","prefix","slice","max","suffixScore","suffix","posScore","hint","abs","quoteWeight","scoredMatches","map","m","score","sort","a","b"],"mappings":"+GA+BSA,OAAOC,KAAMC,IAAKC,mBAGrBC,SAAW,EACXC,aAAe,IACE,IAAdD,WAEa,KADlBA,SAAWH,KAAKK,QAAQJ,IAAKE,aAE3BC,aAAaE,KAAK,CAChBC,MAAOJ,SACPK,IAAKL,SAAWF,IAAIQ,OACpBC,OAAQ,IAEVP,UAAY,UAGZC,aAAaK,OAAS,EACjBL,cAKF,wBAAaJ,KAAMC,IAAKC,oBAUxBS,eAAeX,KAAMC,YAIT,IAAfA,IAAIQ,QAAgC,IAAhBT,KAAKS,OACpB,EAMF,EAHSV,OAAOC,KAAMC,IAAKA,IAAIQ,QAGlB,GAAGC,OAAST,IAAIQ,4FAkBXT,KAAMY,WAAOC,+DAAU,MAC3B,IAAjBD,MAAMH,cACD,SAYHP,UAAYY,KAAKC,IAAI,IAAKH,MAAMH,OAAS,GAGzCO,QAAUjB,OAAOC,KAAMY,MAAOV,cAEb,IAAnBc,QAAQP,cACH,SASHQ,WAAa,SAAAC,WAMXC,WAAa,EAAID,MAAMR,OAASE,MAAMH,OAEtCW,YAAcP,QAAQQ,OACxBV,eACEX,KAAKsB,MACHR,KAAKS,IAAI,EAAGL,MAAMX,MAAQM,QAAQQ,OAAOZ,QACzCS,MAAMX,OAERM,QAAQQ,QAEV,EACEG,YAAcX,QAAQY,OACxBd,eACEX,KAAKsB,MAAMJ,MAAMV,IAAKU,MAAMV,IAAMK,QAAQY,OAAOhB,QACjDI,QAAQY,QAEV,EAEAC,SAAW,EACa,iBAAjBb,QAAQc,OAEjBD,SAAW,EADIZ,KAAKc,IAAIV,MAAMX,MAAQM,QAAQc,MACpB3B,KAAKS,eA1Bb,GA8BJU,WA7BK,GA8BJC,YA7BI,GA8BJI,YA7BC,EA8BJE,UACGG,IAQbC,cAAgBd,QAAQe,KAAI,SAAAC,SAAM,CACtCzB,MAAOyB,EAAEzB,MACTC,IAAKwB,EAAExB,IACPyB,MAAOhB,WAAWe,cAIpBF,cAAcI,MAAK,SAACC,EAAGC,UAAMA,EAAEH,MAAQE,EAAEF,SAClCH,cAAc"} \ No newline at end of file diff --git a/amd/build/string-match.min.js.map b/amd/build/string-match.min.js.map index 6db4109..c5f3431 100644 --- a/amd/build/string-match.min.js.map +++ b/amd/build/string-match.min.js.map @@ -1 +1 @@ -{"version":3,"file":"string-match.min.js","sources":["../src/string-match.js"],"sourcesContent":["/**\n * Functions for string matching used by the other methods.\n *\n * This code originaly is from the approx-string-match project (https://github.com/robertknight/approx-string-match-js)\n * by Robert Knight wich is released under the MIT License (https://opensource.org/licenses/MIT).\n */\n\n/**\n * Represents a match returned by a call to `search`.\n * @param {string} s - Document text to search\n * @return {string}\n */\n function reverse(s) {\n return s.split(\"\").reverse().join(\"\");\n }\n\n /**\n * Given the ends of approximate matches for `pattern` in `text`, find\n * the start of the matches.\n *\n * @param {string} text\n * @param {string} pattern\n * @param {array} matches\n * @return {obj} Matches with the `start` property set.\n */\n function findMatchStarts(text, pattern, matches) {\n const patRev = reverse(pattern);\n\n return matches.map((m) => {\n // Find start of each match by reversing the pattern and matching segment\n // of text and searching for an approx match with the same number of\n // errors.\n const minStart = Math.max(0, m.end - pattern.length - m.errors);\n const textRev = reverse(text.slice(minStart, m.end));\n\n // If there are multiple possible start points, choose the one that\n // maximizes the length of the match.\n const start = findMatchEnds(textRev, patRev, m.errors).reduce((min, rm) => {\n if (m.end - rm.end < min) {\n return m.end - rm.end;\n }\n return min;\n }, m.end);\n\n return {\n start,\n end: m.end,\n errors: m.errors,\n };\n });\n }\n\n /**\n * Internal context used when calculating blocks of a column.\n */\n // interface Context {\n // /**\n // * Bit-arrays of positive vertical deltas.\n // *\n // * ie. `P[b][i]` is set if the vertical delta for the i'th row in the b'th\n // * block is positive.\n // */\n // P: Uint32Array;\n // /** Bit-arrays of negative vertical deltas. */\n // M: Uint32Array;\n // /** Bit masks with a single bit set indicating the last row in each block. */\n // lastRowMask: Uint32Array;\n // }\n\n /**\n * Return 1 if a number is non-zero or zero otherwise, without using\n * conditional operators.\n *\n * This should get inlined into `advanceBlock` below by the JIT.\n *\n * Adapted from https://stackoverflow.com/a/3912218/434243\n * @param {int} n\n * @return {bool}\n */\n function oneIfNotZero(n) {\n return ((n | -n) >> 31) & 1;\n }\n\n /**\n * Block calculation step of the algorithm.\n *\n * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional\n * checks with bitwise operations as per Section 4.2.3 of [2].\n *\n * @param {obj} ctx - The pattern context object\n * @param {array} peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param {int} b - The block level\n * @param {obj} hIn - Horizontal input delta ∈ {1,0,-1}\n * @return {obj} Horizontal output delta ∈ {1,0,-1}\n */\n function advanceBlock(ctx, peq, b, hIn) {\n let pV = ctx.P[b];\n let mV = ctx.M[b];\n const hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise.\n const eq = peq[b] | hInIsNegative;\n\n // Step 1: Compute horizontal deltas.\n const xV = eq | mV;\n const xH = (((eq & pV) + pV) ^ pV) | eq;\n\n let pH = mV | ~(xH | pV);\n let mH = pV & xH;\n\n // Step 2: Update score (value of last row of this block).\n const hOut =\n oneIfNotZero(pH & ctx.lastRowMask[b]) -\n oneIfNotZero(mH & ctx.lastRowMask[b]);\n\n // Step 3: Update vertical deltas for use when processing next char.\n pH <<= 1;\n mH <<= 1;\n\n mH |= hInIsNegative;\n pH |= oneIfNotZero(hIn) - hInIsNegative; // set pH[0] if hIn > 0\n\n pV = mH | ~(xV | pH);\n mV = pH & xV;\n\n ctx.P[b] = pV;\n ctx.M[b] = mV;\n\n return hOut;\n }\n\n /**\n * Find the ends and error counts for matches of `pattern` in `text`.\n *\n * Only the matches with the lowest error count are reported. Other matches\n * with error counts <= maxErrors are discarded.\n *\n * This is the block-based search algorithm from Fig. 9 on p.410 of [1].\n *\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n function findMatchEnds(text, pattern, maxErrors) {\n if (pattern.length === 0) {\n return [];\n }\n\n // Clamp error count so we can rely on the `maxErrors` and `pattern.length`\n // rows being in the same block below.\n maxErrors = Math.min(maxErrors, pattern.length);\n\n const matches = [];\n\n // Word size.\n const w = 32;\n\n // Index of maximum block level.\n const bMax = Math.ceil(pattern.length / w) - 1;\n\n // Context used across block calculations.\n const ctx = {\n P: new Uint32Array(bMax + 1),\n M: new Uint32Array(bMax + 1),\n lastRowMask: new Uint32Array(bMax + 1),\n };\n ctx.lastRowMask.fill(1 << 31);\n ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w;\n\n // Dummy \"peq\" array for chars in the text which do not occur in the pattern.\n const emptyPeq = new Uint32Array(bMax + 1);\n\n // Map of UTF-16 character code to bit vector indicating positions in the\n // pattern that equal that character.\n const peq = new Map();\n\n // Version of `peq` that only stores mappings for small characters. This\n // allows faster lookups when iterating through the text because a simple\n // array lookup can be done instead of a hash table lookup.\n const asciiPeq = [];\n for (let i = 0; i < 256; i++) {\n asciiPeq.push(emptyPeq);\n }\n\n // Calculate `ctx.peq` - a map of character values to bitmasks indicating\n // positions of that character within the pattern, where each bit represents\n // a position in the pattern.\n for (let c = 0; c < pattern.length; c += 1) {\n const val = pattern.charCodeAt(c);\n if (peq.has(val)) {\n // Duplicate char in pattern.\n continue;\n }\n\n const charPeq = new Uint32Array(bMax + 1);\n peq.set(val, charPeq);\n if (val < asciiPeq.length) {\n asciiPeq[val] = charPeq;\n }\n\n for (let b = 0; b <= bMax; b += 1) {\n charPeq[b] = 0;\n\n // Set all the bits where the pattern matches the current char (ch).\n // For indexes beyond the end of the pattern, always set the bit as if the\n // pattern contained a wildcard char in that position.\n for (let r = 0; r < w; r += 1) {\n const idx = b * w + r;\n if (idx >= pattern.length) {\n continue;\n }\n\n const match = pattern.charCodeAt(idx) === val;\n if (match) {\n charPeq[b] |= 1 << r;\n }\n }\n }\n }\n\n // Index of last-active block level in the column.\n let y = Math.max(0, Math.ceil(maxErrors / w) - 1);\n\n // Initialize maximum error count at bottom of each block.\n const score = new Uint32Array(bMax + 1);\n for (let b = 0; b <= y; b += 1) {\n score[b] = (b + 1) * w;\n }\n score[bMax] = pattern.length;\n\n // Initialize vertical deltas for each block.\n for (let b = 0; b <= y; b += 1) {\n ctx.P[b] = ~0;\n ctx.M[b] = 0;\n }\n\n // Process each char of the text, computing the error count for `w` chars of\n // the pattern at a time.\n for (let j = 0; j < text.length; j += 1) {\n // Lookup the bitmask representing the positions of the current char from\n // the text within the pattern.\n const charCode = text.charCodeAt(j);\n let charPeq;\n\n if (charCode < asciiPeq.length) {\n // Fast array lookup.\n charPeq = asciiPeq[charCode];\n } else {\n // Slower hash table lookup.\n charPeq = peq.get(charCode);\n if (typeof charPeq === \"undefined\") {\n charPeq = emptyPeq;\n }\n }\n\n // Calculate error count for blocks that we definitely have to process for\n // this column.\n let carry = 0;\n for (let b = 0; b <= y; b += 1) {\n carry = advanceBlock(ctx, charPeq, b, carry);\n score[b] += carry;\n }\n\n // Check if we also need to compute an additional block, or if we can reduce\n // the number of blocks processed for the next column.\n if (\n score[y] - carry <= maxErrors &&\n y < bMax &&\n (charPeq[y + 1] & 1 || carry < 0)\n ) {\n // Error count for bottom block is under threshold, increase the number of\n // blocks processed for this column & next by 1.\n y += 1;\n\n ctx.P[y] = ~0;\n ctx.M[y] = 0;\n\n let maxBlockScore;\n if (y === bMax) {\n const remainder = pattern.length % w;\n maxBlockScore = remainder === 0 ? w : remainder;\n } else {\n maxBlockScore = w;\n }\n\n score[y] =\n score[y - 1] +\n maxBlockScore -\n carry +\n advanceBlock(ctx, charPeq, y, carry);\n } else {\n // Error count for bottom block exceeds threshold, reduce the number of\n // blocks processed for the next column.\n while (y > 0 && score[y] >= maxErrors + w) {\n y -= 1;\n }\n }\n\n // If error count is under threshold, report a match.\n if (y === bMax && score[y] <= maxErrors) {\n if (score[y] < maxErrors) {\n // Discard any earlier, worse matches.\n matches.splice(0, matches.length);\n }\n\n matches.push({\n start: -1,\n end: j + 1,\n errors: score[y],\n });\n\n // Because `search` only reports the matches with the lowest error count,\n // we can \"ratchet down\" the max error threshold whenever a match is\n // encountered and thereby save a small amount of work for the remainder\n // of the text.\n maxErrors = score[y];\n }\n }\n\n return matches;\n }\n\n /**\n * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors.\n *\n * Returns the start, and end positions and error counts for each lowest-cost\n * match. Only the \"best\" matches are returned.\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n export default function search(\n text,\n pattern,\n maxErrors\n ) {\n const matches = findMatchEnds(text, pattern, maxErrors);\n return findMatchStarts(text, pattern, matches);\n }"],"names":["reverse","s","split","join","oneIfNotZero","n","advanceBlock","ctx","peq","b","hIn","pV","P","mV","M","hInIsNegative","eq","xV","xH","pH","mH","hOut","lastRowMask","findMatchEnds","text","pattern","maxErrors","length","Math","min","matches","w","bMax","ceil","Uint32Array","fill","emptyPeq","Map","asciiPeq","i","push","c","val","charCodeAt","has","charPeq","set","r","idx","y","max","score","j","charCode","get","carry","maxBlockScore","remainder","splice","start","end","errors","patRev","map","m","minStart","slice","reduce","rm","findMatchStarts"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"string-match.min.js","sources":["../src/string-match.js"],"sourcesContent":["/**\n * Functions for string matching used by the other methods.\n *\n * This code originaly is from the approx-string-match project (https://github.com/robertknight/approx-string-match-js)\n * by Robert Knight wich is released under the MIT License (https://opensource.org/licenses/MIT).\n */\n\n/**\n * Represents a match returned by a call to `search`.\n * @param {string} s - Document text to search\n * @return {string}\n */\n function reverse(s) {\n return s.split(\"\").reverse().join(\"\");\n }\n\n /**\n * Given the ends of approximate matches for `pattern` in `text`, find\n * the start of the matches.\n *\n * @param {string} text\n * @param {string} pattern\n * @param {array} matches\n * @return {obj} Matches with the `start` property set.\n */\n function findMatchStarts(text, pattern, matches) {\n const patRev = reverse(pattern);\n\n return matches.map((m) => {\n // Find start of each match by reversing the pattern and matching segment\n // of text and searching for an approx match with the same number of\n // errors.\n const minStart = Math.max(0, m.end - pattern.length - m.errors);\n const textRev = reverse(text.slice(minStart, m.end));\n\n // If there are multiple possible start points, choose the one that\n // maximizes the length of the match.\n const start = findMatchEnds(textRev, patRev, m.errors).reduce((min, rm) => {\n if (m.end - rm.end < min) {\n return m.end - rm.end;\n }\n return min;\n }, m.end);\n\n return {\n start,\n end: m.end,\n errors: m.errors,\n };\n });\n }\n\n /**\n * Internal context used when calculating blocks of a column.\n */\n // interface Context {\n // /**\n // * Bit-arrays of positive vertical deltas.\n // *\n // * ie. `P[b][i]` is set if the vertical delta for the i'th row in the b'th\n // * block is positive.\n // */\n // P: Uint32Array;\n // /** Bit-arrays of negative vertical deltas. */\n // M: Uint32Array;\n // /** Bit masks with a single bit set indicating the last row in each block. */\n // lastRowMask: Uint32Array;\n // }\n\n /**\n * Return 1 if a number is non-zero or zero otherwise, without using\n * conditional operators.\n *\n * This should get inlined into `advanceBlock` below by the JIT.\n *\n * Adapted from https://stackoverflow.com/a/3912218/434243\n * @param {int} n\n * @return {bool}\n */\n function oneIfNotZero(n) {\n return ((n | -n) >> 31) & 1;\n }\n\n /**\n * Block calculation step of the algorithm.\n *\n * From Fig 8. on p. 408 of [1], additionally optimized to replace conditional\n * checks with bitwise operations as per Section 4.2.3 of [2].\n *\n * @param {obj} ctx - The pattern context object\n * @param {array} peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param {int} b - The block level\n * @param {obj} hIn - Horizontal input delta ∈ {1,0,-1}\n * @return {obj} Horizontal output delta ∈ {1,0,-1}\n */\n function advanceBlock(ctx, peq, b, hIn) {\n let pV = ctx.P[b];\n let mV = ctx.M[b];\n const hInIsNegative = hIn >>> 31; // 1 if hIn < 0 or 0 otherwise.\n const eq = peq[b] | hInIsNegative;\n\n // Step 1: Compute horizontal deltas.\n const xV = eq | mV;\n const xH = (((eq & pV) + pV) ^ pV) | eq;\n\n let pH = mV | ~(xH | pV);\n let mH = pV & xH;\n\n // Step 2: Update score (value of last row of this block).\n const hOut =\n oneIfNotZero(pH & ctx.lastRowMask[b]) -\n oneIfNotZero(mH & ctx.lastRowMask[b]);\n\n // Step 3: Update vertical deltas for use when processing next char.\n pH <<= 1;\n mH <<= 1;\n\n mH |= hInIsNegative;\n pH |= oneIfNotZero(hIn) - hInIsNegative; // Set pH[0] if hIn > 0.\n\n pV = mH | ~(xV | pH);\n mV = pH & xV;\n\n ctx.P[b] = pV;\n ctx.M[b] = mV;\n\n return hOut;\n }\n\n /**\n * Find the ends and error counts for matches of `pattern` in `text`.\n *\n * Only the matches with the lowest error count are reported. Other matches\n * with error counts <= maxErrors are discarded.\n *\n * This is the block-based search algorithm from Fig. 9 on p.410 of [1].\n *\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n function findMatchEnds(text, pattern, maxErrors) {\n if (pattern.length === 0) {\n return [];\n }\n\n // Clamp error count so we can rely on the `maxErrors` and `pattern.length`\n // rows being in the same block below.\n maxErrors = Math.min(maxErrors, pattern.length);\n\n const matches = [];\n\n // Word size.\n const w = 32;\n\n // Index of maximum block level.\n const bMax = Math.ceil(pattern.length / w) - 1;\n\n // Context used across block calculations.\n const ctx = {\n P: new Uint32Array(bMax + 1),\n M: new Uint32Array(bMax + 1),\n lastRowMask: new Uint32Array(bMax + 1),\n };\n ctx.lastRowMask.fill(1 << 31);\n ctx.lastRowMask[bMax] = 1 << (pattern.length - 1) % w;\n\n // Dummy \"peq\" array for chars in the text which do not occur in the pattern.\n const emptyPeq = new Uint32Array(bMax + 1);\n\n // Map of UTF-16 character code to bit vector indicating positions in the\n // pattern that equal that character.\n const peq = new Map();\n\n // Version of `peq` that only stores mappings for small characters. This\n // allows faster lookups when iterating through the text because a simple\n // array lookup can be done instead of a hash table lookup.\n const asciiPeq = [];\n for (let i = 0; i < 256; i++) {\n asciiPeq.push(emptyPeq);\n }\n\n // Calculate `ctx.peq` - a map of character values to bitmasks indicating\n // positions of that character within the pattern, where each bit represents\n // a position in the pattern.\n for (let c = 0; c < pattern.length; c += 1) {\n const val = pattern.charCodeAt(c);\n if (peq.has(val)) {\n // Duplicate char in pattern.\n continue;\n }\n\n const charPeq = new Uint32Array(bMax + 1);\n peq.set(val, charPeq);\n if (val < asciiPeq.length) {\n asciiPeq[val] = charPeq;\n }\n\n for (let b = 0; b <= bMax; b += 1) {\n charPeq[b] = 0;\n\n // Set all the bits where the pattern matches the current char (ch).\n // For indexes beyond the end of the pattern, always set the bit as if the\n // pattern contained a wildcard char in that position.\n for (let r = 0; r < w; r += 1) {\n const idx = b * w + r;\n if (idx >= pattern.length) {\n continue;\n }\n\n const match = pattern.charCodeAt(idx) === val;\n if (match) {\n charPeq[b] |= 1 << r;\n }\n }\n }\n }\n\n // Index of last-active block level in the column.\n let y = Math.max(0, Math.ceil(maxErrors / w) - 1);\n\n // Initialize maximum error count at bottom of each block.\n const score = new Uint32Array(bMax + 1);\n for (let b = 0; b <= y; b += 1) {\n score[b] = (b + 1) * w;\n }\n score[bMax] = pattern.length;\n\n // Initialize vertical deltas for each block.\n for (let b = 0; b <= y; b += 1) {\n ctx.P[b] = ~0;\n ctx.M[b] = 0;\n }\n\n // Process each char of the text, computing the error count for `w` chars of\n // the pattern at a time.\n for (let j = 0; j < text.length; j += 1) {\n // Lookup the bitmask representing the positions of the current char from\n // the text within the pattern.\n const charCode = text.charCodeAt(j);\n let charPeq;\n\n if (charCode < asciiPeq.length) {\n // Fast array lookup.\n charPeq = asciiPeq[charCode];\n } else {\n // Slower hash table lookup.\n charPeq = peq.get(charCode);\n if (typeof charPeq === \"undefined\") {\n charPeq = emptyPeq;\n }\n }\n\n // Calculate error count for blocks that we definitely have to process for\n // this column.\n let carry = 0;\n for (let b = 0; b <= y; b += 1) {\n carry = advanceBlock(ctx, charPeq, b, carry);\n score[b] += carry;\n }\n\n // Check if we also need to compute an additional block, or if we can reduce\n // the number of blocks processed for the next column.\n if (\n score[y] - carry <= maxErrors &&\n y < bMax &&\n (charPeq[y + 1] & 1 || carry < 0)\n ) {\n // Error count for bottom block is under threshold, increase the number of\n // blocks processed for this column & next by 1.\n y += 1;\n\n ctx.P[y] = ~0;\n ctx.M[y] = 0;\n\n let maxBlockScore;\n if (y === bMax) {\n const remainder = pattern.length % w;\n maxBlockScore = remainder === 0 ? w : remainder;\n } else {\n maxBlockScore = w;\n }\n\n score[y] =\n score[y - 1] +\n maxBlockScore -\n carry +\n advanceBlock(ctx, charPeq, y, carry);\n } else {\n // Error count for bottom block exceeds threshold, reduce the number of\n // blocks processed for the next column.\n while (y > 0 && score[y] >= maxErrors + w) {\n y -= 1;\n }\n }\n\n // If error count is under threshold, report a match.\n if (y === bMax && score[y] <= maxErrors) {\n if (score[y] < maxErrors) {\n // Discard any earlier, worse matches.\n matches.splice(0, matches.length);\n }\n\n matches.push({\n start: -1,\n end: j + 1,\n errors: score[y],\n });\n\n // Because `search` only reports the matches with the lowest error count,\n // we can \"ratchet down\" the max error threshold whenever a match is\n // encountered and thereby save a small amount of work for the remainder\n // of the text.\n maxErrors = score[y];\n }\n }\n\n return matches;\n }\n\n /**\n * Search for matches for `pattern` in `text` allowing up to `maxErrors` errors.\n *\n * Returns the start, and end positions and error counts for each lowest-cost\n * match. Only the \"best\" matches are returned.\n * @param {string} text\n * @param {string} pattern\n * @param {array} maxErrors\n * @return {obj} Matches with the `start` property set.\n */\n export default function search(\n text,\n pattern,\n maxErrors\n ) {\n const matches = findMatchEnds(text, pattern, maxErrors);\n return findMatchStarts(text, pattern, matches);\n }"],"names":["reverse","s","split","join","oneIfNotZero","n","advanceBlock","ctx","peq","b","hIn","pV","P","mV","M","hInIsNegative","eq","xV","xH","pH","mH","hOut","lastRowMask","findMatchEnds","text","pattern","maxErrors","length","Math","min","matches","w","bMax","ceil","Uint32Array","fill","emptyPeq","Map","asciiPeq","i","push","c","val","charCodeAt","has","charPeq","set","r","idx","y","max","score","j","charCode","get","carry","maxBlockScore","remainder","splice","start","end","errors","patRev","map","m","minStart","slice","reduce","rm","findMatchStarts"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/text-range.min.js b/amd/build/text-range.min.js index 252cdc8..5c6929d 100644 --- a/amd/build/text-range.min.js +++ b/amd/build/text-range.min.js @@ -1,3 +1,3 @@ -define("mod_margic/text-range",["exports"],(function(_exports){function _slicedToArray(arr,i){return function(arr){if(Array.isArray(arr))return arr}(arr)||function(arr,i){var _i=null==arr?null:"undefined"!=typeof Symbol&&arr[Symbol.iterator]||arr["@@iterator"];if(null==_i)return;var _s,_e,_arr=[],_n=!0,_d=!1;try{for(_i=_i.call(arr);!(_n=(_s=_i.next()).done)&&(_arr.push(_s.value),!i||_arr.length!==i);_n=!0);}catch(err){_d=!0,_e=err}finally{try{_n||null==_i.return||_i.return()}finally{if(_d)throw _e}}return _arr}(arr,i)||function(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(arr,i)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i1?_len-1:0),_key=1;_key<_len;_key++)offsets[_key-1]=arguments[_key];for(var textNode,nextOffset=offsets.shift(),nodeIter=element.ownerDocument.createNodeIterator(element,NodeFilter.SHOW_TEXT),results=[],currentNode=nodeIter.nextNode(),length=0;void 0!==nextOffset&¤tNode;)length+(textNode=currentNode).data.length>nextOffset?(results.push({node:textNode,offset:nextOffset-length}),nextOffset=offsets.shift()):(currentNode=nodeIter.nextNode(),length+=textNode.data.length);for(;void 0!==nextOffset&&textNode&&length===nextOffset;)results.push({node:textNode,offset:textNode.data.length}),nextOffset=offsets.shift();if(void 0!==nextOffset)throw new RangeError("Offset exceeds text length");return results}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.TextRange=_exports.TextPosition=_exports.RESOLVE_FORWARDS=_exports.RESOLVE_BACKWARDS=void 0;_exports.RESOLVE_FORWARDS=1;_exports.RESOLVE_BACKWARDS=2;var TextPosition=function(){function TextPosition(element,offset){if(_classCallCheck(this,TextPosition),offset<0)throw new Error("Offset is invalid");this.element=element,this.offset=offset}return _createClass(TextPosition,[{key:"relativeTo",value:function(parent){if(!parent.contains(this.element))throw new Error("Parent is not an ancestor of current element");for(var el=this.element,offset=this.offset;el!==parent;)offset+=previousSiblingsTextLength(el),el=el.parentElement;return new TextPosition(el,offset)}},{key:"resolve",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return resolveOffsets(this.element,this.offset)[0]}catch(err){if(0===this.offset&&void 0!==options.direction){var tw=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);tw.currentNode=this.element;var forwards=1===options.direction,text=forwards?tw.nextNode():tw.previousNode();if(!text)throw err;return{node:text,offset:forwards?0:text.data.length}}throw err}}}],[{key:"fromCharOffset",value:function(node,offset){switch(node.nodeType){case Node.TEXT_NODE:return TextPosition.fromPoint(node,offset);case Node.ELEMENT_NODE:return new TextPosition(node,offset);default:throw new Error("Node is not an element or text node")}}},{key:"fromPoint",value:function(node,offset){switch(node.nodeType){case Node.TEXT_NODE:if(offset<0||offset>node.data.length)throw new Error("Text node offset is out of range");if(!node.parentElement)throw new Error("Text node has no parent");var textOffset=previousSiblingsTextLength(node)+offset;return new TextPosition(node.parentElement,textOffset);case Node.ELEMENT_NODE:if(offset<0||offset>node.childNodes.length)throw new Error("Child node offset is out of range");for(var _textOffset=0,i=0;iarr.length)&&(len=arr.length);for(var i=0,arr2=new Array(len);i1?_len-1:0),_key=1;_key<_len;_key++)offsets[_key-1]=arguments[_key];for(var textNode,nextOffset=offsets.shift(),nodeIter=element.ownerDocument.createNodeIterator(element,NodeFilter.SHOW_TEXT),results=[],currentNode=nodeIter.nextNode(),length=0;void 0!==nextOffset&¤tNode;)length+(textNode=currentNode).data.length>nextOffset?(results.push({node:textNode,offset:nextOffset-length}),nextOffset=offsets.shift()):(currentNode=nodeIter.nextNode(),length+=textNode.data.length);for(;void 0!==nextOffset&&length===nextOffset;)results.push({node:textNode,offset:textNode.data.length}),nextOffset=offsets.shift();if(void 0!==nextOffset)throw new RangeError("Offset exceeds text length");return results}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.TextRange=_exports.TextPosition=_exports.RESOLVE_FORWARDS=_exports.RESOLVE_BACKWARDS=void 0;_exports.RESOLVE_FORWARDS=1;_exports.RESOLVE_BACKWARDS=2;var TextPosition=function(){function TextPosition(element,offset){if(_classCallCheck(this,TextPosition),offset<0)throw new Error("Offset is invalid");this.element=element,this.offset=offset}return _createClass(TextPosition,[{key:"relativeTo",value:function(parent){if(!parent.contains(this.element))throw new Error("Parent is not an ancestor of current element");for(var el=this.element,offset=this.offset;el!==parent;)offset+=previousSiblingsTextLength(el),el=el.parentElement;return new TextPosition(el,offset)}},{key:"resolve",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};try{return resolveOffsets(this.element,this.offset)[0]}catch(err){if(0===this.offset&&void 0!==options.direction){var tw=document.createTreeWalker(this.element.getRootNode(),NodeFilter.SHOW_TEXT);tw.currentNode=this.element;var forwards=1===options.direction,text=forwards?tw.nextNode():tw.previousNode();if(!text)throw err;return{node:text,offset:forwards?0:text.data.length}}throw err}}}],[{key:"fromCharOffset",value:function(node,offset){switch(node.nodeType){case Node.TEXT_NODE:return TextPosition.fromPoint(node,offset);case Node.ELEMENT_NODE:return new TextPosition(node,offset);default:throw new Error("Node is not an element or text node")}}},{key:"fromPoint",value:function(node,offset){switch(node.nodeType){case Node.TEXT_NODE:if(offset<0||offset>node.data.length)throw new Error("Text node offset is out of range");if(!node.parentElement)throw new Error("Text node has no parent");var textOffset=previousSiblingsTextLength(node)+offset;return new TextPosition(node.parentElement,textOffset);case Node.ELEMENT_NODE:if(offset<0||offset>node.childNodes.length)throw new Error("Child node offset is out of range");for(var _textOffset=0,i=0;i nextOffset) {\n results.push({ node: textNode, offset: nextOffset - length });\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n }\n\n // Boundary case.\n while (nextOffset !== undefined && textNode && length === nextOffset) {\n results.push({ node: textNode, offset: textNode.data.length });\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError('Offset exceeds text length');\n }\n\n return results;\n}\n\nexport let RESOLVE_FORWARDS = 1;\nexport let RESOLVE_BACKWARDS = 2;\n\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\nexport class TextPosition {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n constructor(element, offset) {\n if (offset < 0) {\n throw new Error('Offset is invalid');\n }\n\n /** Element that `offset` is relative to. */\n this.element = element;\n\n /** Character offset from the start of the element's `textContent`. */\n this.offset = offset;\n }\n\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error('Parent is not an ancestor of current element');\n }\n\n let el = this.element;\n let offset = this.offset;\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el = /** @type {Element} */ (el.parentElement);\n }\n\n return new TextPosition(el, offset);\n }\n\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element's text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n resolve(options = {}) {\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n const tw = document.createTreeWalker(\n this.element.getRootNode(),\n NodeFilter.SHOW_TEXT\n );\n tw.currentNode = this.element;\n const forwards = options.direction === RESOLVE_FORWARDS;\n const text = /** @type {Text|null} */ (\n forwards ? tw.nextNode() : tw.previousNode()\n );\n if (!text) {\n throw err;\n }\n return { node: text, offset: forwards ? 0 : text.data.length };\n } else {\n throw err;\n }\n }\n }\n\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n static fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n case Node.ELEMENT_NODE:\n return new TextPosition(/** @type {Element} */ (node), offset);\n default:\n throw new Error('Node is not an element or text node');\n }\n }\n\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n static fromPoint(node, offset) {\n\n switch (node.nodeType) {\n case Node.TEXT_NODE: {\n if (offset < 0 || offset > /** @type {Text} */ (node).data.length) {\n throw new Error('Text node offset is out of range');\n }\n\n if (!node.parentElement) {\n throw new Error('Text node has no parent');\n }\n\n // Get the offset from the start of the parent element.\n const textOffset = previousSiblingsTextLength(node) + offset;\n\n return new TextPosition(node.parentElement, textOffset);\n }\n case Node.ELEMENT_NODE: {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error('Child node offset is out of range');\n }\n\n // Get the text length before the `offset`th child of element.\n let textOffset = 0;\n for (let i = 0; i < offset; i++) {\n textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(/** @type {Element} */ (node), textOffset);\n }\n default:\n throw new Error('Point is not in an element or text node');\n }\n }\n}\n\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don't affect its text content, without affecting the text content\n * of the range itself.\n */\nexport class TextRange {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n constructor(start, end) {\n this.start = start;\n this.end = end;\n }\n\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n * @return {Range}\n */\n relativeTo(element) {\n return new TextRange(\n this.start.relativeTo(element),\n this.end.relativeTo(element)\n );\n }\n\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to \"shrink\" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n toRange() {\n let start;\n let end;\n\n if (\n this.start.element === this.end.element &&\n this.start.offset <= this.end.offset\n ) {\n // Fast path for start and end points in same element.\n [start, end] = resolveOffsets(\n this.start.element,\n this.start.offset,\n this.end.offset\n );\n } else {\n start = this.start.resolve({direction: RESOLVE_FORWARDS});\n end = this.end.resolve({direction: RESOLVE_BACKWARDS});\n }\n\n const range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n static fromRange(range) {\n const start = TextPosition.fromPoint(\n range.startContainer,\n range.startOffset\n );\n const end = TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n * @return {Range}\n */\n static fromOffsets(root, start, end) {\n return new TextRange(\n new TextPosition(root, start),\n new TextPosition(root, end)\n );\n }\n}\n"],"names":["nodeTextLength","node","nodeType","Node","ELEMENT_NODE","TEXT_NODE","textContent","length","previousSiblingsTextLength","sibling","previousSibling","resolveOffsets","element","offsets","textNode","nextOffset","shift","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","results","currentNode","nextNode","undefined","data","push","offset","RangeError","TextPosition","Error","parent","contains","this","el","parentElement","options","err","direction","tw","document","createTreeWalker","getRootNode","forwards","text","previousNode","fromPoint","textOffset","childNodes","i","TextRange","start","end","relativeTo","resolve","range","Range","setStart","setEnd","startContainer","startOffset","endContainer","endOffset","root"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"text-range.min.js","sources":["../src/text-range.js"],"sourcesContent":["/**\n * Functions for handling text-ranges used by the other methods.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\n/**\n * Return the combined length of text nodes contained in `node`.\n *\n * @param {Node} node\n * @return {string}\n */\nfunction nodeTextLength(node) {\n switch (node.nodeType) {\n case Node.ELEMENT_NODE:\n case Node.TEXT_NODE:\n // Nb. `textContent` excludes text in comments and processing instructions\n // when called on a parent element, so we don't need to subtract that here.\n\n return /** @type {string} */ (node.textContent).length;\n default:\n return 0;\n }\n}\n\n/**\n * Return the total length of the text of all previous siblings of `node`.\n *\n * @param {Node} node\n * @return {int}\n */\nfunction previousSiblingsTextLength(node) {\n let sibling = node.previousSibling;\n let length = 0;\n\n while (sibling) {\n length += nodeTextLength(sibling);\n sibling = sibling.previousSibling;\n }\n\n return length;\n}\n\n/**\n * Resolve one or more character offsets within an element to (text node, position)\n * pairs.\n *\n * @param {Element} element\n * @param {number[]} offsets - Offsets, which must be sorted in ascending order\n * @return {{ node: Text, offset: number }[]}\n */\nfunction resolveOffsets(element, ...offsets) {\n\n let nextOffset = offsets.shift();\n const nodeIter = /** @type {Document} */ (\n element.ownerDocument\n ).createNodeIterator(element, NodeFilter.SHOW_TEXT);\n const results = [];\n\n let currentNode = nodeIter.nextNode();\n let textNode;\n let length = 0;\n\n // Find the text node containing the `nextOffset`th character from the start\n // of `element`.\n while (nextOffset !== undefined && currentNode) {\n textNode = /** @type {Text} */ (currentNode);\n\n if (length + textNode.data.length > nextOffset) {\n results.push({node: textNode, offset: nextOffset - length});\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n }\n\n // Boundary case.\n while (nextOffset !== undefined && length === nextOffset) {\n results.push({node: textNode, offset: textNode.data.length});\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError('Offset exceeds text length');\n }\n\n return results;\n}\n\nexport let RESOLVE_FORWARDS = 1;\nexport let RESOLVE_BACKWARDS = 2;\n\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\nexport class TextPosition {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n constructor(element, offset) {\n if (offset < 0) {\n throw new Error('Offset is invalid');\n }\n\n /** Element that `offset` is relative to. */\n this.element = element;\n\n /** Character offset from the start of the element's `textContent`. */\n this.offset = offset;\n }\n\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error('Parent is not an ancestor of current element');\n }\n\n let el = this.element;\n let offset = this.offset;\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el = /** @type {Element} */ (el.parentElement);\n }\n\n return new TextPosition(el, offset);\n }\n\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element's text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n resolve(options = {}) {\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n const tw = document.createTreeWalker(\n this.element.getRootNode(),\n NodeFilter.SHOW_TEXT\n );\n tw.currentNode = this.element;\n const forwards = options.direction === RESOLVE_FORWARDS;\n const text = /** @type {Text|null} */ (\n forwards ? tw.nextNode() : tw.previousNode()\n );\n if (!text) {\n throw err;\n }\n return {node: text, offset: forwards ? 0 : text.data.length};\n } else {\n throw err;\n }\n }\n }\n\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n static fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n case Node.ELEMENT_NODE:\n return new TextPosition(/** @type {Element} */ (node), offset);\n default:\n throw new Error('Node is not an element or text node');\n }\n }\n\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n static fromPoint(node, offset) {\n\n switch (node.nodeType) {\n case Node.TEXT_NODE: {\n if (offset < 0 || offset > /** @type {Text} */ (node).data.length) {\n throw new Error('Text node offset is out of range');\n }\n\n if (!node.parentElement) {\n throw new Error('Text node has no parent');\n }\n\n // Get the offset from the start of the parent element.\n const textOffset = previousSiblingsTextLength(node) + offset;\n\n return new TextPosition(node.parentElement, textOffset);\n }\n case Node.ELEMENT_NODE: {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error('Child node offset is out of range');\n }\n\n // Get the text length before the `offset`th child of element.\n let textOffset = 0;\n for (let i = 0; i < offset; i++) {\n textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(/** @type {Element} */ (node), textOffset);\n }\n default:\n throw new Error('Point is not in an element or text node');\n }\n }\n}\n\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don't affect its text content, without affecting the text content\n * of the range itself.\n */\nexport class TextRange {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n constructor(start, end) {\n this.start = start;\n this.end = end;\n }\n\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n * @return {Range}\n */\n relativeTo(element) {\n return new TextRange(\n this.start.relativeTo(element),\n this.end.relativeTo(element)\n );\n }\n\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to \"shrink\" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n toRange() {\n let start;\n let end;\n\n if (\n this.start.element === this.end.element &&\n this.start.offset <= this.end.offset\n ) {\n // Fast path for start and end points in same element.\n [start, end] = resolveOffsets(\n this.start.element,\n this.start.offset,\n this.end.offset\n );\n } else {\n start = this.start.resolve({direction: RESOLVE_FORWARDS});\n end = this.end.resolve({direction: RESOLVE_BACKWARDS});\n }\n\n const range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n static fromRange(range) {\n const start = TextPosition.fromPoint(\n range.startContainer,\n range.startOffset\n );\n const end = TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n * @return {Range}\n */\n static fromOffsets(root, start, end) {\n return new TextRange(\n new TextPosition(root, start),\n new TextPosition(root, end)\n );\n }\n}\n"],"names":["nodeTextLength","node","nodeType","Node","ELEMENT_NODE","TEXT_NODE","textContent","length","previousSiblingsTextLength","sibling","previousSibling","resolveOffsets","element","offsets","textNode","nextOffset","shift","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","results","currentNode","nextNode","undefined","data","push","offset","RangeError","TextPosition","Error","parent","contains","this","el","parentElement","options","err","direction","tw","document","createTreeWalker","getRootNode","forwards","text","previousNode","fromPoint","textOffset","childNodes","i","TextRange","start","end","relativeTo","resolve","range","Range","setStart","setEnd","startContainer","startOffset","endContainer","endOffset","root"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/types.min.js.map b/amd/build/types.min.js.map index dd7c30c..b537244 100644 --- a/amd/build/types.min.js.map +++ b/amd/build/types.min.js.map @@ -1 +1 @@ -{"version":3,"file":"types.min.js","sources":["../src/types.js"],"sourcesContent":["/**\n * This module exports a set of classes for converting between DOM `Range`\n * objects and different types of selectors. It is mostly a thin wrapper around a\n * set of anchoring libraries. It serves two main purposes:\n *\n * 1. Providing a consistent interface across different types of anchors.\n * 2. Insulating the rest of the code from API changes in the underlying anchoring\n * libraries.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport { matchQuote } from './match-quote';\nimport { TextRange, TextPosition } from './text-range';\nimport { nodeFromXPath, xpathFromNode } from './xpath';\n\n/**\n * @typedef {import('../../types/api').RangeSelector} RangeSelector\n * @typedef {import('../../types/api').TextPositionSelector} TextPositionSelector\n * @typedef {import('../../types/api').TextQuoteSelector} TextQuoteSelector\n */\n\n/**\n * Converts between `RangeSelector` selectors and `Range` objects.\n */\nexport class RangeAnchor {\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n constructor(root, range) {\n this.root = root;\n this.range = range;\n }\n\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n * @return {RangeAnchor}\n */\n static fromRange(root, range) {\n return new RangeAnchor(root, range);\n }\n\n /**\n * Create an anchor from a serialized `RangeSelector` selector.\n *\n * @param {Element} root - A root element from which to anchor.\n * @param {RangeSelector} selector\n * @return {RangeAnchor}\n */\n static fromSelector(root, selector) {\n\n const startContainer = nodeFromXPath(selector.startContainer, root);\n\n if (!startContainer) {\n throw new Error('Failed to resolve startContainer XPath');\n }\n\n const endContainer = nodeFromXPath(selector.endContainer, root);\n if (!endContainer) {\n throw new Error('Failed to resolve endContainer XPath');\n }\n\n const startPos = TextPosition.fromCharOffset(\n startContainer,\n selector.startOffset\n );\n const endPos = TextPosition.fromCharOffset(\n endContainer,\n selector.endOffset\n );\n\n const range = new TextRange(startPos, endPos).toRange();\n return new RangeAnchor(root, range);\n }\n\n toRange() {\n return this.range;\n }\n\n /**\n * @return {RangeSelector}\n */\n toSelector() {\n // \"Shrink\" the range so that it tightly wraps its text. This ensures more\n // predictable output for a given text selection.\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n const textRange = TextRange.fromRange(normalizedRange);\n const startContainer = xpathFromNode(textRange.start.element, this.root);\n const endContainer = xpathFromNode(textRange.end.element, this.root);\n\n return {\n type: 'RangeSelector',\n startContainer,\n startOffset: textRange.start.offset,\n endContainer,\n endOffset: textRange.end.offset,\n };\n }\n}\n\n/**\n * Converts between `TextPositionSelector` selectors and `Range` objects.\n */\nexport class TextPositionAnchor {\n /**\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n constructor(root, start, end) {\n this.root = root;\n this.start = start;\n this.end = end;\n }\n\n /**\n * @param {Element} root\n * @param {Range} range\n * @return {TextPositionAnchor}\n */\n static fromRange(root, range) {\n const textRange = TextRange.fromRange(range).relativeTo(root);\n return new TextPositionAnchor(\n root,\n textRange.start.offset,\n textRange.end.offset\n );\n }\n /**\n * @param {Element} root\n * @param {TextPositionSelector} selector\n * @return {TextPositionAnchor}\n */\n static fromSelector(root, selector) {\n return new TextPositionAnchor(root, selector.start, selector.end);\n }\n\n /**\n * @return {TextPositionSelector}\n */\n toSelector() {\n return {\n type: 'TextPositionSelector',\n start: this.start,\n end: this.end,\n };\n }\n\n toRange() {\n return TextRange.fromOffsets(this.root, this.start, this.end).toRange();\n }\n}\n\n/**\n * @typedef QuoteMatchOptions\n * @prop {number} [hint] - Expected position of match in text. See `matchQuote`.\n */\n\n/**\n * Converts between `TextQuoteSelector` selectors and `Range` objects.\n */\nexport class TextQuoteAnchor {\n /**\n * @param {Element} root - A root element from which to anchor.\n * @param {string} exact\n * @param {Object} context\n * @param {string} [context.prefix]\n * @param {string} [context.suffix]\n */\n constructor(root, exact, context = {}) {\n this.root = root;\n this.exact = exact;\n this.context = context;\n }\n\n /**\n * Create a `TextQuoteAnchor` from a range.\n *\n * Will throw if `range` does not contain any text nodes.\n *\n * @param {Element} root\n * @param {Range} range\n * @return {TextQuoteAnchor}\n */\n static fromRange(root, range) {\n const text = /** @type {string} */ (root.textContent);\n const textRange = TextRange.fromRange(range).relativeTo(root);\n\n const start = textRange.start.offset;\n const end = textRange.end.offset;\n\n // Number of characters around the quote to capture as context. We currently\n // always use a fixed amount, but it would be better if this code was aware\n // of logical boundaries in the document (paragraph, article etc.) to avoid\n // capturing text unrelated to the quote.\n //\n // In regular prose the ideal content would often be the surrounding sentence.\n // This is a natural unit of meaning which enables displaying quotes in\n // context even when the document is not available. We could use `Intl.Segmenter`\n // for this when available.\n const contextLen = 32;\n\n return new TextQuoteAnchor(root, text.slice(start, end), {\n prefix: text.slice(Math.max(0, start - contextLen), start),\n suffix: text.slice(end, Math.min(text.length, end + contextLen)),\n });\n }\n\n /**\n * @param {Element} root\n * @param {TextQuoteSelector} selector\n * @return {TextQuoteAnchor}\n */\n static fromSelector(root, selector) {\n const { prefix, suffix } = selector;\n return new TextQuoteAnchor(root, selector.exact, {prefix, suffix});\n }\n\n /**\n * @return {TextQuoteSelector}\n */\n toSelector() {\n return {\n type: 'TextQuoteSelector',\n exact: this.exact,\n prefix: this.context.prefix,\n suffix: this.context.suffix,\n };\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextQuoteAnchor}\n */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextPositionAnchor}\n */\n toPositionAnchor(options = {}) {\n const text = /** @type {string} */ (this.root.textContent);\n const match = matchQuote(text, this.exact, {\n ...this.context,\n hint: options.hint,\n });\n\n if (!match) {\n throw new Error('Quote not found');\n }\n\n return new TextPositionAnchor(this.root, match.start, match.end);\n }\n}\n"],"names":["RangeAnchor","root","range","this","normalizedRange","TextRange","fromRange","toRange","textRange","startContainer","start","element","endContainer","end","type","startOffset","offset","endOffset","selector","Error","startPos","TextPosition","fromCharOffset","endPos","TextPositionAnchor","fromOffsets","relativeTo","TextQuoteAnchor","exact","context","prefix","suffix","options","toPositionAnchor","text","textContent","match","hint","slice","Math","max","min","length"],"mappings":"01DA2BaA,4CAKCC,KAAMC,8CACXD,KAAOA,UACPC,MAAQA,4DA6Cf,kBACSC,KAAKD,gCAMd,eAIQE,gBAAkBC,qBAAUC,UAAUH,KAAKD,OAAOK,UAElDC,UAAYH,qBAAUC,UAAUF,iBAChCK,gBAAiB,wBAAcD,UAAUE,MAAMC,QAASR,KAAKF,MAC7DW,cAAe,wBAAcJ,UAAUK,IAAIF,QAASR,KAAKF,YAExD,CACLa,KAAM,gBACNL,eAAAA,eACAM,YAAaP,UAAUE,MAAMM,OAC7BJ,aAAAA,aACAK,UAAWT,UAAUK,IAAIG,mCA3D7B,SAAiBf,KAAMC,cACd,IAAIF,YAAYC,KAAMC,mCAU/B,SAAoBD,KAAMiB,cAElBT,gBAAiB,wBAAcS,SAAST,eAAgBR,UAEzDQ,qBACG,IAAIU,MAAM,8CAGZP,cAAe,wBAAcM,SAASN,aAAcX,UACrDW,mBACG,IAAIO,MAAM,4CAGZC,SAAWC,wBAAaC,eAC5Bb,eACAS,SAASH,aAELQ,OAASF,wBAAaC,eAC1BV,aACAM,SAASD,kBAIJ,IAAIjB,YAAYC,KADT,IAAII,qBAAUe,SAAUG,QAAQhB,mEAkCrCiB,0DAMCvB,KAAMS,MAAOG,mDAClBZ,KAAOA,UACPS,MAAQA,WACRG,IAAMA,oEA4Bb,iBACS,CACLC,KAAM,uBACNJ,MAAOP,KAAKO,MACZG,IAAKV,KAAKU,4BAId,kBACSR,qBAAUoB,YAAYtB,KAAKF,KAAME,KAAKO,MAAOP,KAAKU,KAAKN,qCA7BhE,SAAiBN,KAAMC,WACfM,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,aACjD,IAAIuB,mBACTvB,KACAO,UAAUE,MAAMM,OAChBR,UAAUK,IAAIG,oCAQlB,SAAoBf,KAAMiB,iBACjB,IAAIM,mBAAmBvB,KAAMiB,SAASR,MAAOQ,SAASL,kFA2BpDc,oDAQC1B,KAAM2B,WAAOC,+DAAU,8CAC5B5B,KAAOA,UACP2B,MAAQA,WACRC,QAAUA,qEAiDjB,iBACS,CACLf,KAAM,oBACNc,MAAOzB,KAAKyB,MACZE,OAAQ3B,KAAK0B,QAAQC,OACrBC,OAAQ5B,KAAK0B,QAAQE,+BAQzB,eAAQC,+DAAU,UACT7B,KAAK8B,iBAAiBD,SAASzB,0CAOxC,eAAiByB,+DAAU,GACnBE,KAA8B/B,KAAKF,KAAKkC,YACxCC,OAAQ,0BAAWF,KAAM/B,KAAKyB,qCAC/BzB,KAAK0B,aACRQ,KAAML,QAAQK,YAGXD,YACG,IAAIjB,MAAM,0BAGX,IAAIK,mBAAmBrB,KAAKF,KAAMmC,MAAM1B,MAAO0B,MAAMvB,gCArE9D,SAAiBZ,KAAMC,WACfgC,KAA8BjC,KAAKkC,YACnC3B,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,MAElDS,MAAQF,UAAUE,MAAMM,OACxBH,IAAML,UAAUK,IAAIG,cAanB,IAAIW,gBAAgB1B,KAAMiC,KAAKI,MAAM5B,MAAOG,KAAM,CACvDiB,OAAQI,KAAKI,MAAMC,KAAKC,IAAI,EAAG9B,MAHd,IAGmCA,OACpDqB,OAAQG,KAAKI,MAAMzB,IAAK0B,KAAKE,IAAIP,KAAKQ,OAAQ7B,IAJ7B,mCAarB,SAAoBZ,KAAMiB,cAChBY,OAAmBZ,SAAnBY,OAAQC,OAAWb,SAAXa,cACT,IAAIJ,gBAAgB1B,KAAMiB,SAASU,MAAO,CAACE,OAAAA,OAAQC,OAAAA"} \ No newline at end of file +{"version":3,"file":"types.min.js","sources":["../src/types.js"],"sourcesContent":["/**\n * This module exports a set of classes for converting between DOM `Range`\n * objects and different types of selectors. It is mostly a thin wrapper around a\n * set of anchoring libraries. It serves two main purposes:\n *\n * 1. Providing a consistent interface across different types of anchors.\n * 2. Insulating the rest of the code from API changes in the underlying anchoring\n * libraries.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\nimport {matchQuote} from './match-quote';\nimport {TextRange, TextPosition} from './text-range';\nimport {nodeFromXPath, xpathFromNode} from './xpath';\n\n/**\n * @typedef {import('../../types/api').RangeSelector} RangeSelector\n * @typedef {import('../../types/api').TextPositionSelector} TextPositionSelector\n * @typedef {import('../../types/api').TextQuoteSelector} TextQuoteSelector\n */\n\n/**\n * Converts between `RangeSelector` selectors and `Range` objects.\n */\nexport class RangeAnchor {\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n */\n constructor(root, range) {\n this.root = root;\n this.range = range;\n }\n\n /**\n * @param {Node} root - A root element from which to anchor.\n * @param {Range} range - A range describing the anchor.\n * @return {RangeAnchor}\n */\n static fromRange(root, range) {\n return new RangeAnchor(root, range);\n }\n\n /**\n * Create an anchor from a serialized `RangeSelector` selector.\n *\n * @param {Element} root - A root element from which to anchor.\n * @param {RangeSelector} selector\n * @return {RangeAnchor}\n */\n static fromSelector(root, selector) {\n\n const startContainer = nodeFromXPath(selector.startContainer, root);\n\n if (!startContainer) {\n throw new Error('Failed to resolve startContainer XPath');\n }\n\n const endContainer = nodeFromXPath(selector.endContainer, root);\n if (!endContainer) {\n throw new Error('Failed to resolve endContainer XPath');\n }\n\n const startPos = TextPosition.fromCharOffset(\n startContainer,\n selector.startOffset\n );\n const endPos = TextPosition.fromCharOffset(\n endContainer,\n selector.endOffset\n );\n\n const range = new TextRange(startPos, endPos).toRange();\n return new RangeAnchor(root, range);\n }\n\n toRange() {\n return this.range;\n }\n\n /**\n * @return {RangeSelector}\n */\n toSelector() {\n // \"Shrink\" the range so that it tightly wraps its text. This ensures more\n // predictable output for a given text selection.\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n const textRange = TextRange.fromRange(normalizedRange);\n const startContainer = xpathFromNode(textRange.start.element, this.root);\n const endContainer = xpathFromNode(textRange.end.element, this.root);\n\n return {\n type: 'RangeSelector',\n startContainer,\n startOffset: textRange.start.offset,\n endContainer,\n endOffset: textRange.end.offset,\n };\n }\n}\n\n/**\n * Converts between `TextPositionSelector` selectors and `Range` objects.\n */\nexport class TextPositionAnchor {\n /**\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n */\n constructor(root, start, end) {\n this.root = root;\n this.start = start;\n this.end = end;\n }\n\n /**\n * @param {Element} root\n * @param {Range} range\n * @return {TextPositionAnchor}\n */\n static fromRange(root, range) {\n const textRange = TextRange.fromRange(range).relativeTo(root);\n return new TextPositionAnchor(\n root,\n textRange.start.offset,\n textRange.end.offset\n );\n }\n /**\n * @param {Element} root\n * @param {TextPositionSelector} selector\n * @return {TextPositionAnchor}\n */\n static fromSelector(root, selector) {\n return new TextPositionAnchor(root, selector.start, selector.end);\n }\n\n /**\n * @return {TextPositionSelector}\n */\n toSelector() {\n return {\n type: 'TextPositionSelector',\n start: this.start,\n end: this.end,\n };\n }\n\n toRange() {\n return TextRange.fromOffsets(this.root, this.start, this.end).toRange();\n }\n}\n\n/**\n * @typedef QuoteMatchOptions\n * @prop {number} [hint] - Expected position of match in text. See `matchQuote`.\n */\n\n/**\n * Converts between `TextQuoteSelector` selectors and `Range` objects.\n */\nexport class TextQuoteAnchor {\n /**\n * @param {Element} root - A root element from which to anchor.\n * @param {string} exact\n * @param {Object} context\n * @param {string} [context.prefix]\n * @param {string} [context.suffix]\n */\n constructor(root, exact, context = {}) {\n this.root = root;\n this.exact = exact;\n this.context = context;\n }\n\n /**\n * Create a `TextQuoteAnchor` from a range.\n *\n * Will throw if `range` does not contain any text nodes.\n *\n * @param {Element} root\n * @param {Range} range\n * @return {TextQuoteAnchor}\n */\n static fromRange(root, range) {\n const text = /** @type {string} */ (root.textContent);\n const textRange = TextRange.fromRange(range).relativeTo(root);\n\n const start = textRange.start.offset;\n const end = textRange.end.offset;\n\n // Number of characters around the quote to capture as context. We currently\n // always use a fixed amount, but it would be better if this code was aware\n // of logical boundaries in the document (paragraph, article etc.) to avoid\n // capturing text unrelated to the quote.\n //\n // In regular prose the ideal content would often be the surrounding sentence.\n // This is a natural unit of meaning which enables displaying quotes in\n // context even when the document is not available. We could use `Intl.Segmenter`\n // for this when available.\n const contextLen = 32;\n\n return new TextQuoteAnchor(root, text.slice(start, end), {\n prefix: text.slice(Math.max(0, start - contextLen), start),\n suffix: text.slice(end, Math.min(text.length, end + contextLen)),\n });\n }\n\n /**\n * @param {Element} root\n * @param {TextQuoteSelector} selector\n * @return {TextQuoteAnchor}\n */\n static fromSelector(root, selector) {\n const {prefix, suffix} = selector;\n return new TextQuoteAnchor(root, selector.exact, {prefix, suffix});\n }\n\n /**\n * @return {TextQuoteSelector}\n */\n toSelector() {\n return {\n type: 'TextQuoteSelector',\n exact: this.exact,\n prefix: this.context.prefix,\n suffix: this.context.suffix,\n };\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextQuoteAnchor}\n */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\n * @return {TextPositionAnchor}\n */\n toPositionAnchor(options = {}) {\n const text = /** @type {string} */ (this.root.textContent);\n const match = matchQuote(text, this.exact, {\n ...this.context,\n hint: options.hint,\n });\n\n if (!match) {\n throw new Error('Quote not found');\n }\n\n return new TextPositionAnchor(this.root, match.start, match.end);\n }\n}\n"],"names":["RangeAnchor","root","range","this","normalizedRange","TextRange","fromRange","toRange","textRange","startContainer","start","element","endContainer","end","type","startOffset","offset","endOffset","selector","Error","startPos","TextPosition","fromCharOffset","endPos","TextPositionAnchor","fromOffsets","relativeTo","TextQuoteAnchor","exact","context","prefix","suffix","options","toPositionAnchor","text","textContent","match","hint","slice","Math","max","min","length"],"mappings":"01DA2BaA,4CAKCC,KAAMC,8CACXD,KAAOA,UACPC,MAAQA,4DA6Cf,kBACSC,KAAKD,gCAMd,eAIQE,gBAAkBC,qBAAUC,UAAUH,KAAKD,OAAOK,UAElDC,UAAYH,qBAAUC,UAAUF,iBAChCK,gBAAiB,wBAAcD,UAAUE,MAAMC,QAASR,KAAKF,MAC7DW,cAAe,wBAAcJ,UAAUK,IAAIF,QAASR,KAAKF,YAExD,CACLa,KAAM,gBACNL,eAAAA,eACAM,YAAaP,UAAUE,MAAMM,OAC7BJ,aAAAA,aACAK,UAAWT,UAAUK,IAAIG,mCA3D7B,SAAiBf,KAAMC,cACd,IAAIF,YAAYC,KAAMC,mCAU/B,SAAoBD,KAAMiB,cAElBT,gBAAiB,wBAAcS,SAAST,eAAgBR,UAEzDQ,qBACG,IAAIU,MAAM,8CAGZP,cAAe,wBAAcM,SAASN,aAAcX,UACrDW,mBACG,IAAIO,MAAM,4CAGZC,SAAWC,wBAAaC,eAC5Bb,eACAS,SAASH,aAELQ,OAASF,wBAAaC,eAC1BV,aACAM,SAASD,kBAIJ,IAAIjB,YAAYC,KADT,IAAII,qBAAUe,SAAUG,QAAQhB,mEAkCrCiB,0DAMCvB,KAAMS,MAAOG,mDAClBZ,KAAOA,UACPS,MAAQA,WACRG,IAAMA,oEA4Bb,iBACS,CACLC,KAAM,uBACNJ,MAAOP,KAAKO,MACZG,IAAKV,KAAKU,4BAId,kBACSR,qBAAUoB,YAAYtB,KAAKF,KAAME,KAAKO,MAAOP,KAAKU,KAAKN,qCA7BhE,SAAiBN,KAAMC,WACfM,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,aACjD,IAAIuB,mBACTvB,KACAO,UAAUE,MAAMM,OAChBR,UAAUK,IAAIG,oCAQlB,SAAoBf,KAAMiB,iBACjB,IAAIM,mBAAmBvB,KAAMiB,SAASR,MAAOQ,SAASL,kFA2BpDc,oDAQC1B,KAAM2B,WAAOC,+DAAU,8CAC5B5B,KAAOA,UACP2B,MAAQA,WACRC,QAAUA,qEAiDjB,iBACS,CACLf,KAAM,oBACNc,MAAOzB,KAAKyB,MACZE,OAAQ3B,KAAK0B,QAAQC,OACrBC,OAAQ5B,KAAK0B,QAAQE,+BAQzB,eAAQC,+DAAU,UACT7B,KAAK8B,iBAAiBD,SAASzB,0CAOxC,eAAiByB,+DAAU,GACnBE,KAA8B/B,KAAKF,KAAKkC,YACxCC,OAAQ,0BAAWF,KAAM/B,KAAKyB,qCAC/BzB,KAAK0B,aACRQ,KAAML,QAAQK,YAGXD,YACG,IAAIjB,MAAM,0BAGX,IAAIK,mBAAmBrB,KAAKF,KAAMmC,MAAM1B,MAAO0B,MAAMvB,gCArE9D,SAAiBZ,KAAMC,WACfgC,KAA8BjC,KAAKkC,YACnC3B,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,MAElDS,MAAQF,UAAUE,MAAMM,OACxBH,IAAML,UAAUK,IAAIG,cAanB,IAAIW,gBAAgB1B,KAAMiC,KAAKI,MAAM5B,MAAOG,KAAM,CACvDiB,OAAQI,KAAKI,MAAMC,KAAKC,IAAI,EAAG9B,MAHd,IAGmCA,OACpDqB,OAAQG,KAAKI,MAAMzB,IAAK0B,KAAKE,IAAIP,KAAKQ,OAAQ7B,IAJ7B,mCAarB,SAAoBZ,KAAMiB,cACjBY,OAAkBZ,SAAlBY,OAAQC,OAAUb,SAAVa,cACR,IAAIJ,gBAAgB1B,KAAMiB,SAASU,MAAO,CAACE,OAAAA,OAAQC,OAAAA"} \ No newline at end of file diff --git a/amd/src/highlighting.js b/amd/src/highlighting.js index 4daab7d..516093b 100644 --- a/amd/src/highlighting.js +++ b/amd/src/highlighting.js @@ -77,11 +77,11 @@ export function describe(root, range) { const textRange = TextRange.fromRange(range); - anchor = { annotation, target, range: textRange }; + anchor = {annotation, target, range: textRange}; } catch (err) { - anchor = { annotation, target }; + anchor = {annotation, target}; } return anchor; @@ -436,10 +436,8 @@ function isNodeInRange(range, node) { for (var i = 0; i < highlights.length; i++) { if (highlights[i].parentNode) { - //var pn = highlights[i].parentNode; const children = Array.from(highlights[i].childNodes); replaceWith(highlights[i], children); - //pn.normalize(); // To Be removed? } } } diff --git a/amd/src/match-quote.js b/amd/src/match-quote.js index 610fe4f..be6a5f6 100644 --- a/amd/src/match-quote.js +++ b/amd/src/match-quote.js @@ -1,3 +1,11 @@ +/** + * Functions for quote matching for the annotations and highlighting. + * + * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client) + * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause), + * sometimes referred to as the "Simplified BSD License". + */ + import approxSearch from './string-match'; /** diff --git a/amd/src/string-match.js b/amd/src/string-match.js index 7765be8..8637003 100644 --- a/amd/src/string-match.js +++ b/amd/src/string-match.js @@ -116,7 +116,7 @@ mH <<= 1; mH |= hInIsNegative; - pH |= oneIfNotZero(hIn) - hInIsNegative; // set pH[0] if hIn > 0 + pH |= oneIfNotZero(hIn) - hInIsNegative; // Set pH[0] if hIn > 0. pV = mH | ~(xV | pH); mV = pH & xV; diff --git a/amd/src/text-range.js b/amd/src/text-range.js index 1be3b3d..3f9d6d7 100644 --- a/amd/src/text-range.js +++ b/amd/src/text-range.js @@ -10,12 +10,13 @@ * Return the combined length of text nodes contained in `node`. * * @param {Node} node + * @return {string} */ function nodeTextLength(node) { switch (node.nodeType) { case Node.ELEMENT_NODE: case Node.TEXT_NODE: - // nb. `textContent` excludes text in comments and processing instructions + // Nb. `textContent` excludes text in comments and processing instructions // when called on a parent element, so we don't need to subtract that here. return /** @type {string} */ (node.textContent).length; @@ -28,6 +29,7 @@ function nodeTextLength(node) { * Return the total length of the text of all previous siblings of `node`. * * @param {Node} node + * @return {int} */ function previousSiblingsTextLength(node) { let sibling = node.previousSibling; @@ -67,7 +69,7 @@ function resolveOffsets(element, ...offsets) { textNode = /** @type {Text} */ (currentNode); if (length + textNode.data.length > nextOffset) { - results.push({ node: textNode, offset: nextOffset - length }); + results.push({node: textNode, offset: nextOffset - length}); nextOffset = offsets.shift(); } else { currentNode = nodeIter.nextNode(); @@ -76,8 +78,8 @@ function resolveOffsets(element, ...offsets) { } // Boundary case. - while (nextOffset !== undefined && textNode && length === nextOffset) { - results.push({ node: textNode, offset: textNode.data.length }); + while (nextOffset !== undefined && length === nextOffset) { + results.push({node: textNode, offset: textNode.data.length}); nextOffset = offsets.shift(); } @@ -174,7 +176,7 @@ export class TextPosition { if (!text) { throw err; } - return { node: text, offset: forwards ? 0 : text.data.length }; + return {node: text, offset: forwards ? 0 : text.data.length}; } else { throw err; } diff --git a/amd/src/types.js b/amd/src/types.js index 547fef4..d77f530 100644 --- a/amd/src/types.js +++ b/amd/src/types.js @@ -12,9 +12,9 @@ * sometimes referred to as the "Simplified BSD License". */ -import { matchQuote } from './match-quote'; -import { TextRange, TextPosition } from './text-range'; -import { nodeFromXPath, xpathFromNode } from './xpath'; +import {matchQuote} from './match-quote'; +import {TextRange, TextPosition} from './text-range'; +import {nodeFromXPath, xpathFromNode} from './xpath'; /** * @typedef {import('../../types/api').RangeSelector} RangeSelector @@ -218,7 +218,7 @@ export class TextQuoteAnchor { * @return {TextQuoteAnchor} */ static fromSelector(root, selector) { - const { prefix, suffix } = selector; + const {prefix, suffix} = selector; return new TextQuoteAnchor(root, selector.exact, {prefix, suffix}); } diff --git a/annotations.php b/annotations.php index 751dbb5..49c10db 100644 --- a/annotations.php +++ b/annotations.php @@ -24,7 +24,7 @@ use core\output\notification; -require_once("../../config.php"); +require(__DIR__.'/../../config.php'); require_once($CFG->dirroot . '/mod/margic/locallib.php'); global $DB, $CFG; @@ -91,7 +91,7 @@ $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); // Delete annotation. -if (has_capability('mod/margic:makeannotations', $context) && $deleteannotation !== 0) { +if (has_capability('mod/margic:deleteannotations', $context) && $deleteannotation !== 0) { require_sesskey(); global $USER; diff --git a/classes/local/helper.php b/classes/local/helper.php index e589166..81565fc 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -327,7 +327,7 @@ public static function margic_get_editor_and_attachment_options($course, $contex // If maxfiles would be set to an int and more files are given the editor saves them all but saves the overcouting incorrect so that white box is diaplayed. - // For a file attachments field (not really needed here?). + // For a file attachments field (not really needed here). $attachmentoptions = array( 'subdirs' => false, 'maxfiles' => 1, @@ -379,13 +379,13 @@ public static function margic_get_edittime_options($moduleinstance) { } /** - * Check for existing rating entry in mdl_rating for the current user. + * Check for existing rating entry in mdl_rating. * * @param array $ratingoptions An array of current entry data. * @return array $rec An entry was found, so return it for update. */ public static function check_rating_entry($ratingoptions) { - global $USER, $DB, $CFG; + global $DB, $CFG; $params = array(); $params['contextid'] = $ratingoptions->contextid; $params['component'] = $ratingoptions->component; @@ -411,9 +411,7 @@ public static function check_rating_entry($ratingoptions) { } /** - * Check for existing rating entry in mdl_rating for the current user. - * - * Used in view.php. + * Return aggregation string for rating in margic. * * @param int $aggregate The margic rating method. * @return string $aggregatestr Return the language string for the rating method. @@ -440,7 +438,7 @@ public static function get_margic_aggregation($aggregate) { $aggregatestr = get_string('aggregatesum', 'rating'); break; default: - $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270. + $aggregatestr = 'AVG'; // Default to this to avoid real breakage. debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method '.$aggregate, DEBUG_DEVELOPER); } return $aggregatestr; diff --git a/classes/output/margic_error_summary.php b/classes/output/margic_error_summary.php index 46d22b4..1661362 100644 --- a/classes/output/margic_error_summary.php +++ b/classes/output/margic_error_summary.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Class containing data for margic annotations summary + * Class containing data for margic error summary * * @package mod_margic * @copyright 2022 coactum GmbH @@ -29,7 +29,7 @@ use stdClass; /** - * Class containing data for margic annotations summary + * Class containing data for margic error summary * * @package mod_margic * @copyright 2022 coactum GmbH diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index f660d65..9d1344a 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -228,8 +228,6 @@ public static function export_user_data(approved_contextlist $contextlist) { // Write it. writer::with_context($context)->export_data([], $contextdata); - // Todo: Store related metadata. - // Write generic module intro files. helper::export_context_files($context, $user); diff --git a/db/access.php b/db/access.php index 114101d..bb73544 100644 --- a/db/access.php +++ b/db/access.php @@ -71,7 +71,18 @@ ), 'mod/margic:makeannotations' => array( - 'riskbitmask' => RISK_XSS | RISK_SPAM | RISK_DATALOSS, + 'riskbitmask' => RISK_XSS | RISK_SPAM, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), + + 'mod/margic:deleteannotations' => array( + 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( diff --git a/db/install.php b/db/install.php index 4834a15..8d33c9b 100644 --- a/db/install.php +++ b/db/install.php @@ -30,6 +30,7 @@ function xmldb_margic_install() { global $DB; + // Create default errortype templates. $errortype = new stdClass(); $errortype->id = 1; $errortype->timecreated = time(); diff --git a/db/install.xml b/db/install.xml index 6a56e93..61fcfa9 100644 --- a/db/install.xml +++ b/db/install.xml @@ -90,10 +90,10 @@ - - - - + + + + diff --git a/edit.php b/edit.php index 3b6340f..2b2cbc4 100644 --- a/edit.php +++ b/edit.php @@ -27,7 +27,7 @@ use core\output\notification; use mod_margic\output\margic_entry; -require_once("../../config.php"); +require(__DIR__.'/../../config.php'); require_once('./edit_form.php'); require_once($CFG->dirroot . '/mod/margic/locallib.php'); diff --git a/error_summary.php b/error_summary.php index 9430805..e41da21 100644 --- a/error_summary.php +++ b/error_summary.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Prints the annotation summary for the margic instance. + * Prints the error summary for the margic instance. * * @package mod_margic * @copyright 2022 coactum GmbH diff --git a/errortypes.php b/errortypes.php index b343423..642329c 100644 --- a/errortypes.php +++ b/errortypes.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Prints the annotation type form for the margic instance. + * Prints the error type form for the margic instance. * * @package mod_margic * @copyright 2022 coactum GmbH diff --git a/grade_entry.php b/grade_entry.php index b653149..02a4005 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -25,7 +25,7 @@ use core\output\notification; use mod_margic\local\helper; -require_once("../../config.php"); +require(__DIR__.'/../../config.php'); require_once($CFG->dirroot . '/mod/margic/grading_form.php'); require_once($CFG->dirroot . '/mod/margic/locallib.php'); diff --git a/index.php b/index.php index 918c0ff..14a9490 100644 --- a/index.php +++ b/index.php @@ -22,7 +22,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once(__DIR__ . "/../../config.php"); +require(__DIR__.'/../../config.php'); require_once("lib.php"); $id = required_param('id', PARAM_INT); // Course. diff --git a/lang/de/margic.php b/lang/de/margic.php index ac35fee..8cea6a0 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -15,11 +15,11 @@ // along with Moodle. If not, see . /** - * Strings for component 'margic', language 'de', version '3.9'. + * Strings for component 'margic', language 'de'. * * @package mod_margic * @category string - * @copyright 1999 Martin Dougiamas and contributors + * @copyright 2022 coactum GmbH * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -35,7 +35,7 @@ $string['eventfeedbackupdated'] = 'Feedback zu Margic Eintrag aktualisiert'; $string['eventinvalidaccess'] = 'Unberechtigter Zugriff'; -// Common +// Common. $string['modulename'] = 'Margic'; $string['modulenameplural'] = 'Margics'; $string['modulename_help'] = 'In der Aktivität Margic können Teilnehmerinnen und Teilnehmer unbeschränkt Einträge anlegen welche dann von Lehrenden bewertet und annotiert werden können. @@ -55,13 +55,13 @@ $string['modulename_link'] = 'mod/margic/view'; $string['pluginadministration'] = 'Margic Administration'; -// General errors +// General errors. $string['erraccessdenied'] = 'Zugang verweigert'; $string['generalerrorinsert'] = 'Speichern des neuen Margic Eintrags fehlgeschlagen.'; $string['incorrectcourseid'] = 'Inkorrekte Kurs-ID'; $string['incorrectmodule'] = 'Inkorrekte Kurs-Modul-ID'; -// Entry (template) +// Entry (template). $string['entry'] = 'Eintrag'; $string['revision'] = 'Überarbeitung'; $string['baseentry'] = 'Originaleintrag'; @@ -81,7 +81,7 @@ $string['toggleolderversions'] = 'Ältere Versionen ein- oder ausblenden'; $string['hoverannotation'] = 'Annotation hervorheben'; -// View (and template) +// View (and template). $string['overview'] = 'Übersicht'; $string['viewentries'] = 'Einträge ansehen'; $string['startnewentry'] = 'Neuer Eintrag'; @@ -113,7 +113,7 @@ $string['viewallentries'] = 'Alle Einträge ansehen'; $string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; -// Annotations +// Annotations. $string['annotationcreated'] = 'Erstellt am {$a}'; $string['annotationmodified'] = 'Bearbeitet am {$a}'; $string['editannotation'] = 'Bearbeiten'; @@ -130,7 +130,7 @@ $string['annotationsarefetched'] = 'Annotationen werden geladen'; $string['reloadannotations'] = 'Annotationen neu laden'; -// mod_form +// Form: mod_form. $string['margicname'] = 'Name der Margic'; $string['margicdescription'] = 'Beschreibung des Margics'; $string['margicopentime'] = 'Startzeit'; @@ -140,7 +140,7 @@ $string['annotationareawidth_help'] = 'Die Breite des Annotationsbereichs in Prozent. Mindestens 20 und maximal 80 Prozent.'; $string['errannotationareawidthinvalid'] = 'Breite ungültig (Minimum: {$a->minwidth}, Maximum: {$a->maxwidth}).'; -// edit_form +// Form: edit_form. $string['addnewentry'] = 'Neuen Eintrag anlegen'; $string['editentry'] = 'Eintrag bearbeiten'; $string['margicentrydate'] = 'Datum für diesen Eintrag festlegen'; @@ -150,7 +150,7 @@ $string['timecreatedinvalid'] = 'Änderung fehlgeschlagen. Es gibt bereits jüngere Versionen dieses Beitrags.'; $string['entryadded'] = 'Eintrag angelegt'; -// grading_form +// Form: grading_form. $string['gradeingradebook'] = 'Aktuelle Bewertung aus der Bewertungsübersicht'; $string['feedbackingradebook'] = 'Aktuelles Feedback aus der Bewertungsübersicht'; $string['savedrating'] = 'Gespeicherte Bewertung für diesen Eintrag'; @@ -162,7 +162,7 @@ $string['errnograder'] = 'Kein Bewerter.'; $string['errnofeedbackorratingdisabled'] = 'Keine Rückmeldung oder Bewertung ist deaktiviert.'; -// error_summary +// Error summary. $string['errorsummary'] = 'Fehlerauswertung'; $string['participant'] = 'TeilnehmerIn'; $string['backtooverview'] = 'Zurück zur Übersicht'; @@ -190,7 +190,7 @@ $string['prioritychanged'] = 'Reihenfolge geändert'; $string['prioritynotchanged'] = 'Reihenfolge konnte nicht geändert werden'; -// errortypes_form +// Form: errortypes_form. $string['annotationcolor'] = 'Farbe des Fehlertyps'; $string['standardtype'] = 'Standard Fehlertyp'; $string['manualtype'] = 'Manueller Fehlertyp'; @@ -206,11 +206,11 @@ $string['explanationhexcolor_help'] = 'Die Farbe des Fehlertypen als Hexadezimalwert. Dieser besteht aus genau 6 Zeichen (A-F sowie 0-9) und repräsentiert eine Farbe. Den Hexwert von beliebigen Farben kann man z. B. unter https://www.w3schools.com/colors/colors_picker.asp herausfinden.'; $string['explanationstandardtype'] = 'Hier kann ausgewählt werden, ob der Fehlertyp ein Standardtyp sein soll. In diesem Fall kann er von allen Lehrenden für ihre Margics ausgewählt und dann in diesen verwendet werden. Andernfalls kann er nur von Ihnen selbst in Ihren Margics verwendet werden.'; -// Calendar +// Calendar. $string['calendarend'] = '{$a} schließt'; $string['calendarstart'] = '{$a} öffnet'; -// csv export +// CSV export. $string['pluginname'] = 'Margic'; $string['userid'] = 'Nutzer-ID'; $string['timecreated'] = 'Zeitpunkt der Erstellung'; @@ -234,20 +234,21 @@ $string['margic:editdefaulterrortypes'] = 'Standardfehlertyp Vorlagen bearbeiten'; $string['margic:viewannotations'] = 'Annotationen ansehen'; $string['margic:makeannotations'] = 'Annotationen anlegen'; +$string['margic:deleteannotations'] = 'Annotationen löschen'; -// Recent activity +// Recent activity. $string['newmargicentries'] = 'Neue Margic Einträge'; -// User complete +// User complete. $string['noentry'] = 'Kein Eintrag'; -// Search +// Search. $string['search'] = 'Suche'; $string['search:activity'] = 'Margic - Informationen zur Aktivität'; $string['search:entry'] = 'Margic Einträge'; $string['search:feedback'] = 'Feedback zum Margic Eintrag'; -// Default error type templates +// Default error type templates. $string['grammar_verb'] = 'Grammatik: Verbform'; $string['grammar_syntax'] = 'Grammatik: Satzbau'; $string['grammar_congruence'] = 'Grammatik: Kongruenz'; @@ -257,13 +258,13 @@ $string['punctuation'] = 'Interpunktion'; $string['other'] = 'Sonstiges'; -// lib +// Lib. $string['deletealluserdata'] = 'Alle Einträge, deren Annotationen, Dateien und Bewertungen löschen'; $string['alluserdatadeleted'] = 'Alle Einträge, deren Annotationen, Dateien und Bewertungen wurden entfernt'; $string['deleteerrortypes'] = 'Fehlertypen löschen'; $string['errortypesdeleted'] = 'Fehlertypen gelöscht'; -// messages +// Messages. $string['messageprovider:gradingmessages'] = 'Systemnachrichten bei der Bewertung von Einträgen'; $string['sendgradingmessage'] = 'Ersteller/in des Eintrags sofort über die Bewertung benachrichtigen'; $string['gradingmailsubject'] = 'Feedback zu Margic-Eintrag erhalten'; diff --git a/lang/en/margic.php b/lang/en/margic.php index 4aceb3b..9b41691 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -22,6 +22,7 @@ * @copyright 2022 coactum GmbH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ + defined('MOODLE_INTERNAL') || die(); // Events. @@ -34,7 +35,7 @@ $string['eventfeedbackupdated'] = 'Margic feedback updated'; $string['eventinvalidaccess'] = 'Invalid access'; -// Common +// Common. $string['modulename'] = 'Margic'; $string['modulenameplural'] = 'Margics'; $string['modulename_help'] = 'In the Margic activity, participants can create unlimited entries which can then be evaluated and annotated by teachers. @@ -54,13 +55,13 @@ $string['modulename_link'] = 'mod/margic/view'; $string['pluginadministration'] = 'Margic administration'; -// General errors +// General errors. $string['erraccessdenied'] = 'Access denied'; $string['generalerrorinsert'] = 'Could not save the new Margic entry.'; $string['incorrectcourseid'] = 'Course ID is incorrect'; $string['incorrectmodule'] = 'Course Module ID is incorrect'; -// Entry (template) +// Entry (template). $string['entry'] = 'Entry'; $string['revision'] = 'Revision'; $string['baseentry'] = 'Base entry'; @@ -80,7 +81,7 @@ $string['toggleolderversions'] = 'Toggle older versions of the entry'; $string['hoverannotation'] = 'Hover annotation'; -// View (and template) +// View (and template). $string['overview'] = 'Overview'; $string['viewentries'] = 'View entries'; $string['startnewentry'] = 'New entry'; @@ -112,7 +113,7 @@ $string['viewallentries'] = 'View all entries'; $string['viewallmargics'] = 'View all margics in course'; -// Annotations +// Annotations. $string['annotationcreated'] = 'Created at {$a}'; $string['annotationmodified'] = 'Modified at {$a}'; $string['editannotation'] = 'Edit'; @@ -129,7 +130,7 @@ $string['annotationsarefetched'] = 'Annotations being loaded'; $string['reloadannotations'] = 'Reload annotations'; -// mod_form +// Form: mod_form. $string['margicname'] = 'Name of the Margic'; $string['margicdescription'] = 'Description of the Margic'; $string['margicopentime'] = 'Open time'; @@ -139,7 +140,7 @@ $string['annotationareawidth_help'] = 'The width of the annotation area in percent. Minimum 20 and maximum 80 percent.'; $string['errannotationareawidthinvalid'] = 'Width invalid (minimum: {$a->minwidth}%, maximum: {$a->maxwidth}%).'; -// edit_form +// Form: edit_form. $string['addnewentry'] = 'Add new entry'; $string['editentry'] = 'Edit entry'; $string['margicentrydate'] = 'Set date for this entry'; @@ -149,7 +150,7 @@ $string['timecreatedinvalid'] = 'Change failed. There are already younger versions of this entry.'; $string['entryadded'] = 'Entry added'; -// grading_form +// Form: grading_form. $string['gradeingradebook'] = 'Current rating from gradebook'; $string['feedbackingradebook'] = 'Current feedback from gradebook'; $string['savedrating'] = 'Rating saved for this entry'; @@ -161,7 +162,7 @@ $string['errnograder'] = 'No grader.'; $string['errnofeedbackorratingdisabled'] = 'No feedback or rating disabled.'; -// error_summary +// Error summary. $string['errorsummary'] = 'Error summary'; $string['participant'] = 'Participant'; $string['backtooverview'] = 'Back to overview'; @@ -189,7 +190,7 @@ $string['prioritychanged'] = 'Order changed'; $string['prioritynotchanged'] = 'Order could not be changed'; -// errortypes_form +// Form: errortypes_form. $string['annotationcolor'] = 'Color of the error type'; $string['standardtype'] = 'Standard error type'; $string['manualtype'] = 'Manual error type'; @@ -205,11 +206,11 @@ $string['explanationhexcolor_help'] = 'The color of the error type as hexadecimal value. This consists of exactly 6 characters (A-F as well as 0-9) and represents a color. You can find out the hexadecimal value of any color, for example, at https://www.w3schools.com/colors/colors_picker.asp.'; $string['explanationstandardtype'] = 'Here you can select whether the error type should be a default type. In this case teachers can select it as error type that can be used in their Margics. Otherwise, only you can add this error type to your Margics.'; -// Calendar +// Calendar. $string['calendarend'] = '{$a} closes'; $string['calendarstart'] = '{$a} opens'; -// csv export +// CSV export. $string['pluginname'] = 'Margic'; $string['userid'] = 'User id'; $string['timecreated'] = 'Time created'; @@ -233,20 +234,21 @@ $string['margic:editdefaulterrortypes'] = 'Edit default error type templates'; $string['margic:viewannotations'] = 'View annotations'; $string['margic:makeannotations'] = 'Make annotations'; +$string['margic:deleteannotations'] = 'Delete annotations'; -// Recent activity +// Recent activity. $string['newmargicentries'] = 'New Margic entries'; -// User complete +// User complete. $string['noentry'] = 'No entry'; -// Search +// Search. $string['search'] = 'Search'; $string['search:activity'] = 'Margic - activity information'; $string['search:entry'] = 'Margic entries'; $string['search:feedback'] = 'Feedback to Margic entries'; -// Default error type templates +// Default error type templates. $string['grammar_verb'] = 'Grammar: Verb form'; $string['grammar_syntax'] = 'Grammar: Syntax'; $string['grammar_congruence'] = 'Grammar: Congruence'; @@ -256,13 +258,13 @@ $string['punctuation'] = 'Punctuation'; $string['other'] = 'Other'; -// lib +// Lib. $string['deletealluserdata'] = 'Delete all entries, annotations, files and ratings'; $string['alluserdatadeleted'] = 'All entries, annotations, files and ratings are deleted'; $string['deleteerrortypes'] = 'Delete error types'; $string['errortypesdeleted'] = 'Error types deleted'; -// messages +// Messages. $string['messageprovider:gradingmessages'] = 'Notifications when entries are rated'; $string['sendgradingmessage'] = 'Notify the creator of the entry immediately about the rating'; $string['gradingmailsubject'] = 'Received feedback for Margic entry'; diff --git a/lib.php b/lib.php index f2e6494..e227be9 100644 --- a/lib.php +++ b/lib.php @@ -222,6 +222,7 @@ function margic_supports($feature) { return true; case FEATURE_BACKUP_MOODLE2: return true; + default: return null; } diff --git a/locallib.php b/locallib.php index 7854922..ef2c2dd 100644 --- a/locallib.php +++ b/locallib.php @@ -73,8 +73,6 @@ class margic { /** @var array Array of error messages encountered during the execution of margic related operations. */ private $errors = array(); - /** @var array Temp helper array with entry nodes sorted by occurance */ - private $nodepositions = array(); /** * Constructor for the base margic class. * diff --git a/mod_form.php b/mod_form.php index a4faff8..9f0bb97 100644 --- a/mod_form.php +++ b/mod_form.php @@ -27,7 +27,7 @@ require_once($CFG->dirroot . '/course/moodleform_mod.php'); /** - * margic settings form. + * Margic activity settings form. * * @package mod_margic * @copyright 2022 coactum GmbH diff --git a/pix/icon.svg b/pix/icon.svg index c473092..4aa78ec 100644 --- a/pix/icon.svg +++ b/pix/icon.svg @@ -22,7 +22,7 @@ preserveAspectRatio="xMinYMid meet" id="svg4564" inkscape:version="0.48.2 r9819" - sodipodi:docname="icon_diari1.svg">image/svg+xml. /** - * This is the css style sheet for margic. + * This is the css style sheet for mod_margic. * * @package mod_margic * @copyright 2022 coactum GmbH @@ -146,7 +146,7 @@ } .path-mod-margic .annotation-box:hover { - box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, .15); + box-shadow: 0 2px 3px 0 rgba(0, 0, 0, .15); } .path-mod-margic #id_submitbutton { @@ -227,7 +227,7 @@ } .path-mod-margic #page { - margin-top: 0px !important; + margin-top: 0 !important; } .path-mod-margic .entry, diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index 45b210a..acf9ba1 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -19,6 +19,7 @@ @template margic/margic_childentry Template for single child entry. + }}
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index fb771ca..4051e57 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -19,13 +19,6 @@ @template margic/margic_entry Template for single entry. - - Context variables required for this template: - * - - Example context (json): - { - } }}
diff --git a/templates/margic_error_summary.mustache b/templates/margic_error_summary.mustache index 97d751b..02d8e7a 100644 --- a/templates/margic_error_summary.mustache +++ b/templates/margic_error_summary.mustache @@ -21,7 +21,7 @@ Annotations summary. }} -
+
diff --git a/version.php b/version.php index 369af80..6dd9c8a 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_margic'; -$plugin->release = '1.2.0'; // User-friendly version number. -$plugin->version = 2022090400; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '1.2.1'; // User-friendly version number. +$plugin->version = 2022090800; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. -$plugin->maturity = MATURITY_BETA; +$plugin->maturity = MATURITY_STABLE; From a879d3a2785b8b1d0278d1327e6fb5be39501561 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Fri, 9 Sep 2022 19:09:32 +0200 Subject: [PATCH 59/60] fix(multiple): multiple small bugfixes --- edit.php | 2 +- errortypes.php | 4 ++++ lang/de/margic.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/edit.php b/edit.php index 2b2cbc4..f256ccd 100644 --- a/edit.php +++ b/edit.php @@ -206,7 +206,7 @@ // Update timemodified for base entry. $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry)); - $baseentry->timemodified = $fromform->timecreated; + $baseentry->timemodified = $newentry->timecreated; $DB->update_record('margic_entries', $baseentry); $newentry->feedback = $entry->feedback; diff --git a/errortypes.php b/errortypes.php index 642329c..da2d79a 100644 --- a/errortypes.php +++ b/errortypes.php @@ -78,6 +78,10 @@ $editedtype = $DB->get_record('margic_errortype_templates', array('id' => $edit)); } else if ($mode == 2) { // If type is margic error type. $editedtype = $DB->get_record('margic_errortypes', array('id' => $edit)); + + if ($moduleinstance->id !== $editedtype->margic) { + redirect($redirecturl, get_string('errortypecantbeedited', 'mod_margic'), null, notification::NOTIFY_ERROR); + } } if ($editedtype && $mode == 2 || diff --git a/lang/de/margic.php b/lang/de/margic.php index 8cea6a0..fad81f1 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -104,7 +104,7 @@ $string['currententry'] = 'Aktuelle Einträge'; $string['oldestentry'] = 'Älteste Einträge'; $string['lowestgradeentry'] = 'Am niedrigsten bewertete Einträge'; -$string['highestgradeentry'] = 'Am höchsten bewertete Beiträge'; +$string['highestgradeentry'] = 'Am höchsten bewertete Einträge'; $string['editingstarts'] = 'Der Bearbeitungszeitraum beginnt am {$a}'; $string['editingends'] = 'Der Bearbeitungszeitraum endet am {$a}'; $string['editingended'] = 'Der Bearbeitungszeitraum endete am {$a}'; From c08c066e66601875783939499d18bc7577ac98d8 Mon Sep 17 00:00:00 2001 From: Daniel Nolte <> Date: Mon, 12 Sep 2022 14:29:26 +0200 Subject: [PATCH 60/60] feat(version): increase version for new release (1.2.2) --- version.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.php b/version.php index 6dd9c8a..04da367 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_margic'; -$plugin->release = '1.2.1'; // User-friendly version number. -$plugin->version = 2022090800; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '1.2.2'; // User-friendly version number. +$plugin->version = 2022091200; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_STABLE;