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}}
+ {{#str}}toggleallannotations, mod_margic{{/str}}
{{#entries.0}}
@@ -153,38 +154,32 @@
{{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}}
+
+
\ 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}}
+
+
\ 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}}
-
\ 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}}
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}}
+
\ 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}}
+
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 '
';
-
}
- // Sort annotations by position and offset of startcontainer.
+ // Sort annotations and find its position.
usort($entry->annotations, "sortannotation");
-
- // Reset nodepositions with empty array for next entry.
- $this->nodepositions = array();
+ $pos = 1;
+ foreach ($entry->annotations as $key => $annotation) {
+ $entry->annotations[$key]->position = $pos;
+ $pos += 1;
+ }
if ($annotationmode) {
// Add annotation form.
From 855485f8c0526959f31dff83f693ced86d924fe7 Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Fri, 2 Sep 2022 14:38:04 +0200
Subject: [PATCH 52/60] fix(multiple): multiple fixes and chore
---
amd/build/annotations.min.js | 2 +-
amd/build/annotations.min.js.map | 2 +-
amd/build/highlighting.min.js | 2 +-
amd/build/highlighting.min.js.map | 2 +-
amd/build/types.min.js | 2 +-
amd/build/types.min.js.map | 2 +-
amd/build/xpath.min.js.map | 2 +-
amd/src/annotations.js | 20 +--
amd/src/highlighting.js | 16 ++-
amd/src/types.js | 4 +-
amd/src/xpath.js | 9 --
.../restore_margic_activity_task.class.php | 4 +-
backup/moodle2/restore_margic_stepslib.php | 9 --
classes/local/helper.php | 33 +----
classes/search/entry.php | 18 +--
db/access.php | 2 +-
db/install.xml | 19 +--
db/log.php | 14 +-
lib.php | 133 ++----------------
renderer.php | 63 ---------
styles.css | 19 ---
version.php | 4 +-
22 files changed, 73 insertions(+), 308 deletions(-)
diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js
index 256ef70..6e20c3e 100644
--- a/amd/build/annotations.min.js
+++ b/amd/build/annotations.min.js
@@ -5,6 +5,6 @@ define("mod_margic/annotations",["exports","jquery","./highlighting"],(function(
* @module mod_margic/annotations
* @copyright 2022 coactum GmbH
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html((0,_jquery.default)("#annotationpreview-"+annotationid).html()),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(console.log("createAnnotation ranges"),console.log(ranges),ranges.collapsed)return null;console.log("createAnnotation -> ROOT"),console.log(root);var rangeSelectors=ranges.map((function(range){return(0,_highlighting.describe)(root,range)}));console.log("rangeSelectors"),console.log(rangeSelectors);var target=rangeSelectors.map((function(selectors){return{selector:selectors}}));console.log("target"),console.log(target);var annotation={target:target};return console.log("Annotation INFORMATION TO SAVE IN THE DB"),console.log(annotation),(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0]),(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotation.exact)}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching annotations")}})}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html((0,_jquery.default)("#annotationpreview-"+annotationid).html()),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;var annotation={target:ranges.map((function(range){return(0,_highlighting.describe)(root,range)})).map((function(selectors){return{selector:selectors}}))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0]),(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotation.exact)}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching 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 3d89bbb..07646e9 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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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 // console.log('mouseup in originaltext');\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n $('#annotationpreview-temp-' + entry).html(newannotation.target[0].selector[2].exact);\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n // console.log('rangeSelectors');\n // console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n // console.log('target');\n // console.log(target);\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n // console.log(newannotation);\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\n\n $('#annotationpreview-' + annotation.id).html(annotation.exact);\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n // console.log('createAnnotation');\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n console.log('createAnnotation ranges');\n console.log(ranges);\n\n if (ranges.collapsed) {\n return null;\n }\n\n console.log('createAnnotation -> ROOT');\n console.log(root);\n\n //const info = await this.getDocumentInfo();\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n console.log('rangeSelectors');\n console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n console.log('target');\n console.log(target);\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n console.log('Annotation INFORMATION TO SAVE IN THE DB');\n console.log(annotation);\n\n anchor(annotation, root);\n\n // console.log('TEMP');\n // console.log(temp);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","console","log","collapsed","rangeSelectors","map","range","target","selectors","selector","annotation","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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 // console.log('mouseup in originaltext');\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n $('#annotationpreview-temp-' + entry).html(newannotation.target[0].selector[2].exact);\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n // console.log('rangeSelectors');\n // console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n // console.log('target');\n // console.log(target);\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n // console.log(newannotation);\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\n\n $('#annotationpreview-' + annotation.id).html(annotation.exact);\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n // console.log('createAnnotation');\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n // console.log('createAnnotation ranges');\n // console.log(ranges);\n\n if (ranges.collapsed) {\n return null;\n }\n\n // console.log('createAnnotation -> ROOT');\n // console.log(root);\n\n //const info = await this.getDocumentInfo();\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n // console.log('rangeSelectors');\n // console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n // console.log('target');\n // console.log(target);\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n // console.log('Annotation INFORMATION TO SAVE IN THE DB');\n // console.log(annotation);\n\n anchor(annotation, root);\n\n // console.log('TEMP');\n // console.log(temp);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","annotation","target","map","range","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","error","alert"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/highlighting.min.js b/amd/build/highlighting.min.js
index ddaf3b4..609d713 100644
--- a/amd/build/highlighting.min.js
+++ b/amd/build/highlighting.min.js
@@ -1,3 +1,3 @@
-define("mod_margic/highlighting",["exports","jquery","./types","./text-range"],(function(_exports,_jquery,_types2,_textRange){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);i1&&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 highlights=[];return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("margic-highlight");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),nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)})),highlights.push(highlightEl)})),highlights}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.endOffset=0}catch(e){return!1}}function querySelector(anchor){var options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return anchor.toRange(options)}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.anchor=function(annotation,root){var highlight=function(anchor){var range=function(anchor){if(!anchor.range)return null;try{return anchor.range.toRange()}catch(_unused){return null}}(anchor);if(range){var highlights=[];(highlights=annotation.annotation?highlightRange(range,annotation.annotation.id,"annotated",annotation.annotation.color):highlightRange(range,!1,"annotated_temp")).forEach((function(h){h._annotation=anchor.annotation})),anchor.highlights=highlights}};annotation.target||(annotation.target=[]);var _step,anchors=annotation.target.map((function(target){if(!target.selector||!target.selector.some((function(s){return"TextQuoteSelector"===s.type})))return{annotation:annotation,target:target};var anchor;try{var range=function(root,selectors){var options=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},position=null,quote=null,range=null;console.log("htmlAnchor -> ROOT"),console.log(root);var _step2,_iterator2=_createForOfIteratorHelper(selectors);try{for(_iterator2.s();!(_step2=_iterator2.n()).done;){var selector=_step2.value;switch(selector.type){case"TextPositionSelector":position=selector,options.hint=position.start;break;case"TextQuoteSelector":quote=selector;break;case"RangeSelector":range=selector}}}catch(err){_iterator2.e(err)}finally{_iterator2.f()}var maybeAssertQuote=function(range){var _quote;if(null!==(_quote=quote)&&void 0!==_quote&&_quote.exact&&range.toString()!==quote.exact)throw new Error("quote mismatch");return range};try{if(range)return querySelector(_types2.RangeAnchor.fromSelector(root,range),options)||maybeAssertQuote}catch(error){try{if(position)return querySelector(_types2.TextPositionAnchor.fromSelector(root,position),options)||maybeAssertQuote}catch(error){try{if(quote)return querySelector(_types2.TextQuoteAnchor.fromSelector(root,quote),options)}catch(error){return!1}}}}(root,target.selector),textRange=_textRange.TextRange.fromRange(range);anchor={annotation:annotation,target:target,range:textRange}}catch(err){console.log("Error in try to find textrange"),console.log(err),anchor={annotation:annotation,target:target}}return anchor})),_iterator=_createForOfIteratorHelper(anchors);try{for(_iterator.s();!(_step=_iterator.n()).done;){var _anchor2=_step.value;highlight(_anchor2)}}catch(err){_iterator.e(err)}finally{_iterator.f()}return annotation.$orphan=anchors.length>0&&anchors.every((function(anchor){return anchor.target.selector&&!anchor.range})),anchors},_exports.describe=function(root,range){for(var types=[_types2.RangeAnchor,_types2.TextPositionAnchor,_types2.TextQuoteAnchor],result=[],_i=0,_types=types;_i<_types.length;_i++){var type=_types[_i];try{var _anchor=type.fromRange(root,range);result.push(_anchor.toSelector())}catch(error){continue}}return result},_exports.removeAllTempHighlights=function(){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);i1&&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 highlights=[];return textNodeSpans.forEach((function(nodes){var highlightEl=document.createElement("margic-highlight");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),nodes[0].parentNode.replaceChild(highlightEl,nodes[0]),nodes.forEach((function(node){return highlightEl.appendChild(node)})),highlights.push(highlightEl)})),highlights}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.endOffset=0}catch(e){return!1}}function querySelector(anchor){var options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return anchor.toRange(options)}function replaceWith(node,replacements){var parent=node.parentNode;replacements.forEach((function(r){return parent.insertBefore(r,node)})),node.remove()}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.anchor=function(annotation,root){var highlight=function(anchor){var range=function(anchor){if(!anchor.range)return null;try{return anchor.range.toRange()}catch(_unused){return null}}(anchor);if(range){var highlights=[];(highlights=annotation.annotation?highlightRange(range,annotation.annotation.id,"annotated",annotation.annotation.color):highlightRange(range,!1,"annotated_temp")).forEach((function(h){h._annotation=anchor.annotation})),anchor.highlights=highlights}};annotation.target||(annotation.target=[]);var _step,anchors=annotation.target.map((function(target){if(!target.selector||!target.selector.some((function(s){return"TextQuoteSelector"===s.type})))return{annotation:annotation,target:target};var anchor;try{var range=function(root,selectors){var _step2,options=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},position=null,quote=null,range=null,_iterator2=_createForOfIteratorHelper(selectors);try{for(_iterator2.s();!(_step2=_iterator2.n()).done;){var selector=_step2.value;switch(selector.type){case"TextPositionSelector":position=selector,options.hint=position.start;break;case"TextQuoteSelector":quote=selector;break;case"RangeSelector":range=selector}}}catch(err){_iterator2.e(err)}finally{_iterator2.f()}var maybeAssertQuote=function(range){var _quote;if(null!==(_quote=quote)&&void 0!==_quote&&_quote.exact&&range.toString()!==quote.exact)throw new Error("quote mismatch");return range};try{if(range)return querySelector(_types2.RangeAnchor.fromSelector(root,range),options)||maybeAssertQuote}catch(error){try{if(position)return querySelector(_types2.TextPositionAnchor.fromSelector(root,position),options)||maybeAssertQuote}catch(error){try{if(quote)return querySelector(_types2.TextQuoteAnchor.fromSelector(root,quote),options)}catch(error){return!1}}}}(root,target.selector),textRange=_textRange.TextRange.fromRange(range);anchor={annotation:annotation,target:target,range:textRange}}catch(err){anchor={annotation:annotation,target:target}}return anchor})),_iterator=_createForOfIteratorHelper(anchors);try{for(_iterator.s();!(_step=_iterator.n()).done;){var _anchor2=_step.value;highlight(_anchor2)}}catch(err){_iterator.e(err)}finally{_iterator.f()}return annotation.$orphan=anchors.length>0&&anchors.every((function(anchor){return anchor.target.selector&&!anchor.range})),anchors},_exports.describe=function(root,range){for(var types=[_types2.RangeAnchor,_types2.TextPositionAnchor,_types2.TextQuoteAnchor],result=[],_i=0,_types=types;_i<_types.length;_i++){var type=_types[_i];try{var _anchor=type.fromRange(root,range);result.push(_anchor.toSelector())}catch(error){continue}}return result},_exports.removeAllTempHighlights=function(){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 // console.log('anchor -> locate');\n // console.log('target');\n // console.log(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 // console.log('anchor -> locate -> after htmlAnchor');\n // console.log('result of htmlAnchor');\n // console.log(range);\n const textRange = TextRange.fromRange(range);\n // console.log('range for anchor');\n // console.log('textRange');\n // console.log(textRange);\n\n anchor = { annotation, target, range: textRange };\n\n // console.log('anchor found');\n // console.log(anchor);\n } catch (err) {\n console.log('Error in try to find textrange');\n console.log(err);\n anchor = { annotation, target };\n }\n\n // console.log('anchor at the end of anchor -> locate');\n // console.log(anchor);\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 // console.log('highlight');\n // console.log('highlight resolveAnchor');\n const range = resolveAnchor(anchor);\n // console.log('range');\n // console.log(range);\n\n if (!range) {\n // console.log('no range');\n return;\n }\n\n // console.log('highlight after resolveAnchor');\n // console.log('range');\n // console.log(range);\n\n // console.log('annotation');\n // console.log(annotation);\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 // console.log('highlights after i should have highlighted range');\n // console.log(highlights);\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n // if (this._focusedAnnotations.has(anchor.annotation.$tag)) {\n // setHighlightsFocused(highlights, true);\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 // console.log('anchors after locate');\n // console.log(anchors);\n\n for (let anchor of anchors) {\n // console.log('before highlighting anchor');\n // console.log('anchor');\n // console.log(anchor);\n highlight(anchor);\n // console.log('after highlighting 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 // console.log('anchor ends');\n // console.log('anchors');\n // console.log(anchors);\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 // console.log('resolveAnchor');\n // console.log('anchor');\n // console.log(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 // console.log('highlightRange');\n // console.log('range');\n // console.log(range);\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 // console.log('querySelector');\n // console.log('anchor');\n // console.log(anchor);\n // console.log('options');\n // console.log(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 console.log('htmlAnchor -> ROOT');\n console.log(root);\n\n // console.log('htmlAnchor()');\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 // console.log('maybeAssertQuote');\n // console.log('range');\n // console.log(range);\n // console.log('quote');\n // console.log(quote);\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n // console.log('range found!');\n // console.log(range);\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n // console.log('range');\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n // console.log('anchor');\n // console.log(anchor);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for RangeAnchor');\n // console.log(queryselector);\n\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n // console.log('position');\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for TextPositionAnchor');\n // console.log(queryselector);\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n // console.log('quote');\n // console.log('htmlAnchor queryselector for TextQuoteAnchor');\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n // console.log(queryselector);\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 // console.log('removeHighlights highlights');\n // console.log(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","console","log","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 // console.log('describe');\n\n for (let type of types) {\n try {\n const anchor = type.fromRange(root, range);\n\n // console.log('type');\n // console.log(type);\n // console.log('anchor');\n // console.log(anchor);\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 // console.log('anchor');\n // console.log('annotation');\n // console.log(annotation);\n\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 // console.log('anchor -> locate');\n // console.log('target');\n // console.log(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 // console.log('anchor -> locate -> after htmlAnchor');\n // console.log('result of htmlAnchor');\n // console.log(range);\n const textRange = TextRange.fromRange(range);\n // console.log('range for anchor');\n // console.log('textRange');\n // console.log(textRange);\n\n anchor = { annotation, target, range: textRange };\n\n // console.log('anchor found');\n // console.log(anchor);\n } catch (err) {\n // console.log('Error in try to find textrange');\n // console.log(err);\n anchor = { annotation, target };\n }\n\n // console.log('anchor at the end of anchor -> locate');\n // console.log(anchor);\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 // console.log('highlight');\n // console.log('highlight resolveAnchor');\n const range = resolveAnchor(anchor);\n // console.log('range');\n // console.log(range);\n\n if (!range) {\n // console.log('no range');\n return;\n }\n\n // console.log('highlight after resolveAnchor');\n // console.log('range');\n // console.log(range);\n\n // console.log('annotation');\n // console.log(annotation);\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 // console.log('highlights after i should have highlighted range');\n // console.log(highlights);\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n // if (this._focusedAnnotations.has(anchor.annotation.$tag)) {\n // setHighlightsFocused(highlights, true);\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 // console.log('anchors after locate');\n // console.log(anchors);\n\n for (let anchor of anchors) {\n // console.log('before highlighting anchor');\n // console.log('anchor');\n // console.log(anchor);\n highlight(anchor);\n // console.log('after highlighting 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 // console.log('anchor ends');\n // console.log('anchors');\n // console.log(anchors);\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 // console.log('resolveAnchor');\n // console.log('anchor');\n // console.log(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 // console.log('highlightRange');\n // console.log('range');\n // console.log(range);\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 // console.log('querySelector');\n // console.log('anchor');\n // console.log(anchor);\n // console.log('options');\n // console.log(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 // console.log('html.js -> anchor() -> selectors');\n // console.log(selectors);\n\n // console.log('htmlAnchor()');\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 // console.log('maybeAssertQuote');\n // console.log('range');\n // console.log(range);\n // console.log('quote');\n // console.log(quote);\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n // console.log('range found!');\n // console.log(range);\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n // console.log('html.js -> anchor() range RangeAnchor.fromSelector');\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n // console.log('anchor');\n // console.log(anchor);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for RangeAnchor');\n // console.log(queryselector);\n\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n\n // console.log('html.js -> anchor() position TextPositionAnchor.fromSelector');\n\n // console.log('position');\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for TextPositionAnchor');\n // console.log(queryselector);\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n\n // console.log('html.js -> anchor() quote TextQuoteAnchor.fromSelector');\n\n // console.log('quote');\n // console.log('htmlAnchor queryselector for TextQuoteAnchor');\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n // console.log(queryselector);\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 // console.log('removeHighlights highlights');\n // console.log(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
diff --git a/amd/build/types.min.js b/amd/build/types.min.js
index 9d83eb3..8fb957e 100644
--- a/amd/build/types.min.js
+++ b/amd/build/types.min.js
@@ -1,3 +1,3 @@
-define("mod_margic/types",["exports","./match-quote","./text-range","./xpath"],(function(_exports,_matchQuote,_textRange,_xpath){function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);enumerableOnly&&(symbols=symbols.filter((function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable}))),keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var i=1;i fromRange root"),console.log(root);var textRange=_textRange.TextRange.fromRange(range).relativeTo(root);return new TextPositionAnchor(root,textRange.start.offset,textRange.end.offset)}},{key:"fromSelector",value:function(root,selector){return new TextPositionAnchor(root,selector.start,selector.end)}}]),TextPositionAnchor}();_exports.TextPositionAnchor=TextPositionAnchor;var TextQuoteAnchor=function(){function TextQuoteAnchor(root,exact){var context=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};_classCallCheck(this,TextQuoteAnchor),this.root=root,this.exact=exact,this.context=context}return _createClass(TextQuoteAnchor,[{key:"toSelector",value:function(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}},{key:"toRange",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(options).toRange()}},{key:"toPositionAnchor",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},text=this.root.textContent,match=(0,_matchQuote.matchQuote)(text,this.exact,_objectSpread(_objectSpread({},this.context),{},{hint:options.hint}));if(!match)throw new Error("Quote not found");return new TextPositionAnchor(this.root,match.start,match.end)}}],[{key:"fromRange",value:function(root,range){var text=root.textContent,textRange=_textRange.TextRange.fromRange(range).relativeTo(root),start=textRange.start.offset,end=textRange.end.offset;return new TextQuoteAnchor(root,text.slice(start,end),{prefix:text.slice(Math.max(0,start-32),start),suffix:text.slice(end,Math.min(text.length,end+32))})}},{key:"fromSelector",value:function(root,selector){var prefix=selector.prefix,suffix=selector.suffix;return new TextQuoteAnchor(root,selector.exact,{prefix:prefix,suffix:suffix})}}]),TextQuoteAnchor}();_exports.TextQuoteAnchor=TextQuoteAnchor}));
+define("mod_margic/types",["exports","./match-quote","./text-range","./xpath"],(function(_exports,_matchQuote,_textRange,_xpath){function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);enumerableOnly&&(symbols=symbols.filter((function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable}))),keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var i=1;i2&&void 0!==arguments[2]?arguments[2]:{};_classCallCheck(this,TextQuoteAnchor),this.root=root,this.exact=exact,this.context=context}return _createClass(TextQuoteAnchor,[{key:"toSelector",value:function(){return{type:"TextQuoteSelector",exact:this.exact,prefix:this.context.prefix,suffix:this.context.suffix}}},{key:"toRange",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.toPositionAnchor(options).toRange()}},{key:"toPositionAnchor",value:function(){var options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},text=this.root.textContent,match=(0,_matchQuote.matchQuote)(text,this.exact,_objectSpread(_objectSpread({},this.context),{},{hint:options.hint}));if(!match)throw new Error("Quote not found");return new TextPositionAnchor(this.root,match.start,match.end)}}],[{key:"fromRange",value:function(root,range){var text=root.textContent,textRange=_textRange.TextRange.fromRange(range).relativeTo(root),start=textRange.start.offset,end=textRange.end.offset;return new TextQuoteAnchor(root,text.slice(start,end),{prefix:text.slice(Math.max(0,start-32),start),suffix:text.slice(end,Math.min(text.length,end+32))})}},{key:"fromSelector",value:function(root,selector){var prefix=selector.prefix,suffix=selector.suffix;return new TextQuoteAnchor(root,selector.exact,{prefix:prefix,suffix:suffix})}}]),TextQuoteAnchor}();_exports.TextQuoteAnchor=TextQuoteAnchor}));
//# sourceMappingURL=types.min.js.map
\ No newline at end of file
diff --git a/amd/build/types.min.js.map b/amd/build/types.min.js.map
index 221d0d0..15f5447 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 */\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 */\n static fromSelector(root, selector) {\n // console.log('RangeAnchor -> fromSelector before nodeFromXPATH');\n const startContainer = nodeFromXPath(selector.startContainer, root);\n // console.log('after nodeFromXPATH');\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 // console.log('types.js -> toSelector this.range');\n // console.log(this.range);\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n // console.log('types.js -> toSelector normalizedRange');\n // console.log(normalizedRange);\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 */\n static fromRange(root, range) {\n console.log('TextPositionAnchor -> fromRange root');\n console.log(root);\n\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 */\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 */\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 */\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 */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\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 // const match = text.match(this.exact);\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","console","log","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,4DA2Cf,kBACSC,KAAKD,gCAMd,eAOQE,gBAAkBC,qBAAUC,UAAUH,KAAKD,OAAOK,UAKlDC,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,mCAhE7B,SAAiBf,KAAMC,cACd,IAAIF,YAAYC,KAAMC,mCAS/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,mEAwCrCiB,0DAMCvB,KAAMS,MAAOG,mDAClBZ,KAAOA,UACPS,MAAQA,WACRG,IAAMA,oEA6Bb,iBACS,CACLC,KAAM,uBACNJ,MAAOP,KAAKO,MACZG,IAAKV,KAAKU,4BAId,kBACSR,qBAAUoB,YAAYtB,KAAKF,KAAME,KAAKO,MAAOP,KAAKU,KAAKN,qCA/BhE,SAAiBN,KAAMC,OACrBwB,QAAQC,IAAI,wCACZD,QAAQC,IAAI1B,UAENO,UAAYH,qBAAUC,UAAUJ,OAAO0B,WAAW3B,aACjD,IAAIuB,mBACTvB,KACAO,UAAUE,MAAMM,OAChBR,UAAUK,IAAIG,oCAOlB,SAAoBf,KAAMiB,iBACjB,IAAIM,mBAAmBvB,KAAMiB,SAASR,MAAOQ,SAASL,kFA2BpDgB,oDAQC5B,KAAM6B,WAAOC,+DAAU,8CAC5B9B,KAAOA,UACP6B,MAAQA,WACRC,QAAUA,qEA+CjB,iBACS,CACLjB,KAAM,oBACNgB,MAAO3B,KAAK2B,MACZE,OAAQ7B,KAAK4B,QAAQC,OACrBC,OAAQ9B,KAAK4B,QAAQE,+BAOzB,eAAQC,+DAAU,UACT/B,KAAKgC,iBAAiBD,SAAS3B,0CAMxC,eAAiB2B,+DAAU,GACnBE,KAA8BjC,KAAKF,KAAKoC,YACxCC,OAAQ,0BAAWF,KAAMjC,KAAK2B,qCAC/B3B,KAAK4B,aACRQ,KAAML,QAAQK,YAGXD,YACG,IAAInB,MAAM,0BAGX,IAAIK,mBAAmBrB,KAAKF,KAAMqC,MAAM5B,MAAO4B,MAAMzB,gCAlE9D,SAAiBZ,KAAMC,WACfkC,KAA8BnC,KAAKoC,YACnC7B,UAAYH,qBAAUC,UAAUJ,OAAO0B,WAAW3B,MAElDS,MAAQF,UAAUE,MAAMM,OACxBH,IAAML,UAAUK,IAAIG,cAanB,IAAIa,gBAAgB5B,KAAMmC,KAAKI,MAAM9B,MAAOG,KAAM,CACvDmB,OAAQI,KAAKI,MAAMC,KAAKC,IAAI,EAAGhC,MAHd,IAGmCA,OACpDuB,OAAQG,KAAKI,MAAM3B,IAAK4B,KAAKE,IAAIP,KAAKQ,OAAQ/B,IAJ7B,mCAYrB,SAAoBZ,KAAMiB,cAChBc,OAAmBd,SAAnBc,OAAQC,OAAWf,SAAXe,cACT,IAAIJ,gBAAgB5B,KAAMiB,SAASY,MAAO,CAAEE,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 */\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 */\n static fromSelector(root, selector) {\n // console.log('RangeAnchor -> fromSelector before nodeFromXPATH');\n const startContainer = nodeFromXPath(selector.startContainer, root);\n // console.log('after nodeFromXPATH');\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 // console.log('types.js -> toSelector this.range');\n // console.log(this.range);\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n // console.log('types.js -> toSelector normalizedRange');\n // console.log(normalizedRange);\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 */\n static fromRange(root, range) {\n // console.log('TextPositionAnchor -> fromRange root');\n // console.log(root);\n\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 */\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 */\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 */\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 */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\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 // const match = text.match(this.exact);\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,4DA2Cf,kBACSC,KAAKD,gCAMd,eAOQE,gBAAkBC,qBAAUC,UAAUH,KAAKD,OAAOK,UAKlDC,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,mCAhE7B,SAAiBf,KAAMC,cACd,IAAIF,YAAYC,KAAMC,mCAS/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,mEAwCrCiB,0DAMCvB,KAAMS,MAAOG,mDAClBZ,KAAOA,UACPS,MAAQA,WACRG,IAAMA,oEA6Bb,iBACS,CACLC,KAAM,uBACNJ,MAAOP,KAAKO,MACZG,IAAKV,KAAKU,4BAId,kBACSR,qBAAUoB,YAAYtB,KAAKF,KAAME,KAAKO,MAAOP,KAAKU,KAAKN,qCA/BhE,SAAiBN,KAAMC,WAIfM,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,aACjD,IAAIuB,mBACTvB,KACAO,UAAUE,MAAMM,OAChBR,UAAUK,IAAIG,oCAOlB,SAAoBf,KAAMiB,iBACjB,IAAIM,mBAAmBvB,KAAMiB,SAASR,MAAOQ,SAASL,kFA2BpDc,oDAQC1B,KAAM2B,WAAOC,+DAAU,8CAC5B5B,KAAOA,UACP2B,MAAQA,WACRC,QAAUA,qEA+CjB,iBACS,CACLf,KAAM,oBACNc,MAAOzB,KAAKyB,MACZE,OAAQ3B,KAAK0B,QAAQC,OACrBC,OAAQ5B,KAAK0B,QAAQE,+BAOzB,eAAQC,+DAAU,UACT7B,KAAK8B,iBAAiBD,SAASzB,0CAMxC,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,gCAlE9D,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,mCAYrB,SAAoBZ,KAAMiB,cAChBY,OAAmBZ,SAAnBY,OAAQC,OAAWb,SAAXa,cACT,IAAIJ,gBAAgB1B,KAAMiB,SAASU,MAAO,CAAEE,OAAAA,OAAQC,OAAAA"}
\ No newline at end of file
diff --git a/amd/build/xpath.min.js.map b/amd/build/xpath.min.js.map
index 2a5a6ab..2b3207f 100644
--- a/amd/build/xpath.min.js.map
+++ b/amd/build/xpath.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"xpath.min.js","sources":["../src/xpath.js"],"sourcesContent":["/**\n * XPATH and DOM functions used for anchoring 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\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 */\nfunction 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 */\nfunction 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 */\nfunction 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 */\nexport function xpathFromNode(node, root) {\n let xpath = '';\n\n // console.log('xpathFromNode');\n // console.log('node');\n // console.log(node);\n // console.log('root');\n // console.log(root);\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 // console.log('xpathFromNode xpath');\n // console.log(xpath);\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} - The child element or null\n */\nfunction 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 */\nfunction evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath =\n 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 */\nexport 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 but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n}\n"],"names":["getPathSegment","node","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","length","child","evaluateSimpleXPath","xpath","root","match","Error","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","document","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","elem","parentNode","replace"],"mappings":"i0CAgDSA,eAAeC,UAChBC,cAnCaD,UACbE,SAAWF,KAAKE,SAASC,cAC3BC,OAASF,eACI,UAAbA,WACFE,OAAS,UAEJA,OA6BMC,CAAYL,MACnBM,aArBiBN,cACnBM,IAAM,EAENC,IAAMP,KACHO,KACDA,IAAIL,WAAaF,KAAKE,WACxBI,KAAO,GAETC,IAAMA,IAAIC,uBAELF,IAWKG,CAAgBT,sBAClBC,iBAAQK,kBA+CXI,eAAeC,QAAST,SAAUU,OACzCV,SAAWA,SAASW,sBAEhBC,YAAc,EACTC,EAAI,EAAGA,EAAIJ,QAAQK,SAASC,OAAQF,IAAK,KAC1CG,MAAQP,QAAQK,SAASD,MAC3BG,MAAMhB,SAASW,gBAAkBX,YACjCY,aACiBF,aACVM,aAKN,cAwBAC,oBAAoBC,MAAOC,WAEqB,OAArDD,MAAME,MAAM,4CAEN,IAAIC,MAAM,wCAGZC,SAAWJ,MAAMK,MAAM,KACzBd,QAAUU,KAIdG,SAASE,uDAEWF,6DAAU,KAArBG,oBACHC,mBACAC,oBAEEC,aAAeH,QAAQI,QAAQ,SACf,IAAlBD,aAAqB,CACvBF,YAAcD,QAAQK,MAAM,EAAGF,kBAEzBG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,UACjEF,aAAeK,SAASD,UAAY,GACjB,SACV,UAGTL,YAAcD,QACdE,aAAe,MAGXX,MAAQR,eAAeC,QAASiB,YAAaC,kBAC9CX,aACI,KAGTP,QAAUO,gEAGLP,gGAaqBS,WAAOC,4DAAOc,SAASC,gBAE1CjB,oBAAoBC,MAAOC,MAClC,MAAOgB,YACAF,SAASG,SACd,IAAMlB,MACNC,KAGA,KACAkB,YAAYC,wBACZ,MACAC,kDA3IwBzC,KAAMqB,UAC9BD,MAAQ,GASRsB,KAAO1C,UACJ0C,OAASrB,MAAM,KACfqB,WACG,IAAInB,MAAM,oCAElBH,MAAQrB,eAAe2C,MAAQ,IAAMtB,MACrCsB,KAAOA,KAAKC,kBAGdvB,OADAA,MAAQ,IAAMA,OACAwB,QAAQ,MAAO"}
\ No newline at end of file
+{"version":3,"file":"xpath.min.js","sources":["../src/xpath.js"],"sourcesContent":["/**\n * XPATH and DOM functions used for anchoring 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\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 */\nfunction 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 */\nfunction 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 */\nfunction 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 */\nexport 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} - The child element or null\n */\nfunction 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 */\nfunction evaluateSimpleXPath(xpath, root) {\n const isSimpleXPath =\n 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 */\nexport 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 but required in Edge Legacy.\n null /* NamespaceResolver */,\n XPathResult.FIRST_ORDERED_NODE_TYPE,\n null /* Result */\n ).singleNodeValue;\n }\n}\n"],"names":["getPathSegment","node","name","nodeName","toLowerCase","result","getNodeName","pos","tmp","previousSibling","getNodePosition","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","length","child","evaluateSimpleXPath","xpath","root","match","Error","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","document","body","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","elem","parentNode","replace"],"mappings":"i0CAgDSA,eAAeC,UAChBC,cAnCaD,UACbE,SAAWF,KAAKE,SAASC,cAC3BC,OAASF,eACI,UAAbA,WACFE,OAAS,UAEJA,OA6BMC,CAAYL,MACnBM,aArBiBN,cACnBM,IAAM,EAENC,IAAMP,KACHO,KACDA,IAAIL,WAAaF,KAAKE,WACxBI,KAAO,GAETC,IAAMA,IAAIC,uBAELF,IAWKG,CAAgBT,sBAClBC,iBAAQK,kBAsCXI,eAAeC,QAAST,SAAUU,OACzCV,SAAWA,SAASW,sBAEhBC,YAAc,EACTC,EAAI,EAAGA,EAAIJ,QAAQK,SAASC,OAAQF,IAAK,KAC1CG,MAAQP,QAAQK,SAASD,MAC3BG,MAAMhB,SAASW,gBAAkBX,YACjCY,aACiBF,aACVM,aAKN,cAwBAC,oBAAoBC,MAAOC,WAEqB,OAArDD,MAAME,MAAM,4CAEN,IAAIC,MAAM,wCAGZC,SAAWJ,MAAMK,MAAM,KACzBd,QAAUU,KAIdG,SAASE,uDAEWF,6DAAU,KAArBG,oBACHC,mBACAC,oBAEEC,aAAeH,QAAQI,QAAQ,SACf,IAAlBD,aAAqB,CACvBF,YAAcD,QAAQK,MAAM,EAAGF,kBAEzBG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,UACjEF,aAAeK,SAASD,UAAY,GACjB,SACV,UAGTL,YAAcD,QACdE,aAAe,MAGXX,MAAQR,eAAeC,QAASiB,YAAaC,kBAC9CX,aACI,KAGTP,QAAUO,gEAGLP,gGAaqBS,WAAOC,4DAAOc,SAASC,gBAE1CjB,oBAAoBC,MAAOC,MAClC,MAAOgB,YACAF,SAASG,SACd,IAAMlB,MACNC,KAGA,KACAkB,YAAYC,wBACZ,MACAC,kDAlIwBzC,KAAMqB,UAC9BD,MAAQ,GAGRsB,KAAO1C,UACJ0C,OAASrB,MAAM,KACfqB,WACG,IAAInB,MAAM,oCAElBH,MAAQrB,eAAe2C,MAAQ,IAAMtB,MACrCsB,KAAOA,KAAKC,kBAGdvB,OADAA,MAAQ,IAAMA,OACAwB,QAAQ,MAAO"}
\ No newline at end of file
diff --git a/amd/src/annotations.js b/amd/src/annotations.js
index 954424a..ef6f079 100644
--- a/amd/src/annotations.js
+++ b/amd/src/annotations.js
@@ -286,36 +286,36 @@ function createAnnotation(root) {
const ranges = [window.getSelection().getRangeAt(0)];
- console.log('createAnnotation ranges');
- console.log(ranges);
+ // console.log('createAnnotation ranges');
+ // console.log(ranges);
if (ranges.collapsed) {
return null;
}
- console.log('createAnnotation -> ROOT');
- console.log(root);
+ // console.log('createAnnotation -> ROOT');
+ // console.log(root);
//const info = await this.getDocumentInfo();
const rangeSelectors = ranges.map(range => describe(root, range));
- console.log('rangeSelectors');
- console.log(rangeSelectors);
+ // console.log('rangeSelectors');
+ // console.log(rangeSelectors);
const target = rangeSelectors.map(selectors => ({
selector: selectors,
}));
- console.log('target');
- console.log(target);
+ // console.log('target');
+ // console.log(target);
/** @type {AnnotationData} */
const annotation = {
target,
};
- console.log('Annotation INFORMATION TO SAVE IN THE DB');
- console.log(annotation);
+ // console.log('Annotation INFORMATION TO SAVE IN THE DB');
+ // console.log(annotation);
anchor(annotation, root);
diff --git a/amd/src/highlighting.js b/amd/src/highlighting.js
index ab89a0e..025c58d 100644
--- a/amd/src/highlighting.js
+++ b/amd/src/highlighting.js
@@ -101,8 +101,8 @@ export function describe(root, range) {
// console.log('anchor found');
// console.log(anchor);
} catch (err) {
- console.log('Error in try to find textrange');
- console.log(err);
+ // console.log('Error in try to find textrange');
+ // console.log(err);
anchor = { annotation, target };
}
@@ -402,8 +402,8 @@ function isNodeInRange(range, node) {
let quote = null;
let range = null;
- console.log('htmlAnchor -> ROOT');
- console.log(root);
+ // console.log('html.js -> anchor() -> selectors');
+ // console.log(selectors);
// console.log('htmlAnchor()');
@@ -447,7 +447,7 @@ function isNodeInRange(range, node) {
try {
if (range) {
- // console.log('range');
+ // console.log('html.js -> anchor() range RangeAnchor.fromSelector');
let anchor = RangeAnchor.fromSelector(root, range);
@@ -469,6 +469,9 @@ function isNodeInRange(range, node) {
} catch (error) {
try {
if (position) {
+
+ // console.log('html.js -> anchor() position TextPositionAnchor.fromSelector');
+
// console.log('position');
let anchor = TextPositionAnchor.fromSelector(root, position);
@@ -486,6 +489,9 @@ function isNodeInRange(range, node) {
} catch (error) {
try {
if (quote) {
+
+ // console.log('html.js -> anchor() quote TextQuoteAnchor.fromSelector');
+
// console.log('quote');
// console.log('htmlAnchor queryselector for TextQuoteAnchor');
diff --git a/amd/src/types.js b/amd/src/types.js
index d4827dd..645ab9c 100644
--- a/amd/src/types.js
+++ b/amd/src/types.js
@@ -128,8 +128,8 @@ export class TextPositionAnchor {
* @param {Range} range
*/
static fromRange(root, range) {
- console.log('TextPositionAnchor -> fromRange root');
- console.log(root);
+ // console.log('TextPositionAnchor -> fromRange root');
+ // console.log(root);
const textRange = TextRange.fromRange(range).relativeTo(root);
return new TextPositionAnchor(
diff --git a/amd/src/xpath.js b/amd/src/xpath.js
index 402b806..bec362b 100644
--- a/amd/src/xpath.js
+++ b/amd/src/xpath.js
@@ -63,12 +63,6 @@ function getPathSegment(node) {
export function xpathFromNode(node, root) {
let xpath = '';
- // console.log('xpathFromNode');
- // console.log('node');
- // console.log(node);
- // console.log('root');
- // console.log(root);
-
/** @type {Node|null} */
let elem = node;
while (elem !== root) {
@@ -81,9 +75,6 @@ export function xpathFromNode(node, root) {
xpath = '/' + xpath;
xpath = xpath.replace(/\/$/, ''); // Remove trailing slash
- // console.log('xpathFromNode xpath');
- // console.log(xpath);
-
return xpath;
}
diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php
index eaad885..36c9ef9 100644
--- a/backup/moodle2/restore_margic_activity_task.class.php
+++ b/backup/moodle2/restore_margic_activity_task.class.php
@@ -93,11 +93,11 @@ public static function define_restore_log_rules() {
// Define the rules.
$rules[] = new restore_log_rule('margic', 'view', 'view.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', 'view.php?id={course_module}', '{margic}');
-
+ $rules[] = new restore_log_rule('margic', 'download entries', 'view.php?id={course_module}', '{margic}');
+ $rules[] = new restore_log_rule('margic', 'invalid access attempt', 'edit.php?id={course_module}', '{margic}');
return $rules;
}
diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php
index 90a0653..0589fe8 100644
--- a/backup/moodle2/restore_margic_stepslib.php
+++ b/backup/moodle2/restore_margic_stepslib.php
@@ -192,20 +192,11 @@ protected function process_margic_entry_rating($data) {
* 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', 'text', 'margic_entry');
- error_log('margic restore after_execute AFTERTEXT');
-
$this->add_related_files('mod_margic', 'feedback', 'margic_entry');
-
- error_log('margic restore after_execute AFTERFEEDBACK');
-
}
}
diff --git a/classes/local/helper.php b/classes/local/helper.php
index d14c68f..8441a08 100644
--- a/classes/local/helper.php
+++ b/classes/local/helper.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * helper utilities for margic.
+ * Helper utilities for margic.
*
* @package mod_margic
* @copyright 2022 coactum GmbH
@@ -302,37 +302,6 @@ public static function download_entries($context, $course, $margic) {
$csv->download_file();
}
- /**
- * Return formatted text.
- *
- * @param array $entry
- * @param array $course
- * @param array $cm
- * @return string $entrytext Text string containing a user entry.
- * @return int $entry-format Format for user entry.
- * @return array $formatoptions Array of options for a user entry.
- */
- public static function margic_format_entry_text($entry, $course = false, $cm = false) {
- if (! $cm) {
- if ($course) {
- $courseid = $course->id;
- } else {
- $courseid = 0;
- }
- $cm = get_coursemodule_from_instance('margic', $entry->margic, $courseid);
- }
-
- $context = context_module::instance($cm->id);
- $entrytext = file_rewrite_pluginfile_urls($entry->text, 'pluginfile.php', $context->id, 'mod_margic', 'entry', $entry->id);
-
- $formatoptions = array(
- 'context' => $context,
- 'noclean' => false,
- 'trusted' => false
- );
- return format_text($entrytext, $entry->format, $formatoptions);
- }
-
/**
* Return the editor and attachment options when editing a margic entry.
*
diff --git a/classes/search/entry.php b/classes/search/entry.php
index ee41fab..8c4c671 100644
--- a/classes/search/entry.php
+++ b/classes/search/entry.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * margic entries search.
+ * Margic entries search.
*
* @package mod_margic
* @copyright 2022 coactum GmbH
@@ -29,7 +29,7 @@
require_once($CFG->dirroot . '/lib/grouplib.php');
/**
- * margic entries search.
+ * Margic entries search.
*
* @package mod_margic
* @copyright 2022 coactum GmbH
@@ -154,12 +154,8 @@ public function get_doc_url(\core_search\document $doc) {
$contextmodule = \context::instance_by_id($doc->get('contextid'));
$entryuserid = $doc->get('userid');
- if ($entryuserid == $USER->id) {
- $url = '/mod/margic/view.php';
- } else {
- // Teachers see student's entries in the report page.
- $url = '/mod/margic/report.php#entry-' . $entryuserid;
- }
+ $url = '/mod/margic/view.php';
+
return new \moodle_url($url, array(
'id' => $contextmodule->instanceid
));
@@ -189,8 +185,8 @@ public function get_context_url(\core_search\document $doc) {
*/
protected function get_entry($entryid) {
global $DB;
- return $DB->get_record_sql("SELECT de.*, d.course FROM {margic_entries} de
- JOIN {margic} d ON d.id = de.margic
- WHERE de.id = ?", array('id' => $entryid), MUST_EXIST);
+ return $DB->get_record_sql("SELECT me.*, m.course FROM {margic_entries} me
+ JOIN {margic} m ON m.id = me.margic
+ WHERE me.id = ?", array('id' => $entryid), MUST_EXIST);
}
}
diff --git a/db/access.php b/db/access.php
index e12b5b5..7190d43 100644
--- a/db/access.php
+++ b/db/access.php
@@ -83,7 +83,7 @@
'mod/margic:viewannotations' => array(
'riskbitmask' => RISK_PERSONAL,
- 'captype' => 'write',
+ 'captype' => 'read',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
'student' => CAP_ALLOW,
diff --git a/db/install.xml b/db/install.xml
index a4dd2a5..efc6e6d 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -1,5 +1,5 @@
-
@@ -14,14 +14,14 @@
-
-
-
+
+
+
-
+
@@ -36,12 +36,12 @@
-
+
-
+
@@ -77,6 +77,8 @@
+
+
@@ -85,7 +87,7 @@
-
+
@@ -113,6 +115,7 @@
+
diff --git a/db/log.php b/db/log.php
index 6fae0a6..71ce052 100644
--- a/db/log.php
+++ b/db/log.php
@@ -38,25 +38,31 @@
),
array(
'module' => 'margic',
- 'action' => 'view responses',
+ 'action' => 'add entry',
'mtable' => 'margic',
'field' => 'name'
),
array(
'module' => 'margic',
- 'action' => 'add entry',
+ 'action' => 'update entry',
'mtable' => 'margic',
'field' => 'name'
),
array(
'module' => 'margic',
- 'action' => 'update entry',
+ 'action' => 'update feedback',
'mtable' => 'margic',
'field' => 'name'
),
array(
'module' => 'margic',
- 'action' => 'update feedback',
+ 'action' => 'download entries',
+ 'mtable' => 'margic',
+ 'field' => 'name'
+ ),
+ array(
+ 'module' => 'margic',
+ 'action' => 'invalid access attempt',
'mtable' => 'margic',
'field' => 'name'
)
diff --git a/lib.php b/lib.php
index 87a89df..45d1ac0 100644
--- a/lib.php
+++ b/lib.php
@@ -15,7 +15,7 @@
// along with Moodle. If not, see .
/**
- * This page opens the current lib instance of margic.
+ * This page opens the current lib instance of mod margic.
*
* @package mod_margic
* @copyright 2022 coactum GmbH
@@ -26,11 +26,11 @@
/**
* Given an object containing all the necessary data,
- * (defined by the form in mod.html) this function
+ * (defined by the form in mod_form.php) this function
* 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 the margic data.
* @return int margic ID.
*/
function margic_add_instance($margic) {
@@ -81,7 +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 the margic data.
* @return boolean True if successful.
*/
function margic_update_instance($margic) {
@@ -196,7 +196,6 @@ function margic_delete_instance($id) {
* @uses FEATURE_MOD_INTRO
* @uses FEATURE_SHOW_DESCRIPTION
* @uses FEATURE_GRADE_HAS_GRADE
- * @uses FEATURE_GRADE_OUTCOMES
* @uses FEATURE_RATE
* @uses FEATURE_GROUPS
* @uses FEATURE_GROUPINGS
@@ -213,8 +212,6 @@ function margic_supports($feature) {
return true;
case FEATURE_GRADE_HAS_GRADE:
return true;
- case FEATURE_GRADE_OUTCOMES:
- return false;
case FEATURE_RATE:
return true;
case FEATURE_GROUPS:
@@ -230,47 +227,9 @@ function margic_supports($feature) {
}
}
-/**
- * List the actions that correspond to a view of this module.
- * This is used by the participation report.
- *
- * Note: This is not used by new logging system. Event with
- * crud = 'r' and edulevel = LEVEL_PARTICIPATING will
- * be considered as view action.
- *
- * @return array
- */
-function margic_get_view_actions() {
- return array(
- 'view',
- 'view all',
- 'view responses'
- );
-}
-
-/**
- * List the actions that correspond to a post of this module.
- * This is used by the participation report.
- *
- * Note: This is not used by new logging system. Event with
- * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
- * will be considered as post action.
- *
- * @return array
- */
-function margic_get_post_actions() {
- return array(
- 'add entry',
- 'update entry',
- 'update feedback'
- );
-}
-
/**
* Returns a summary of data activity of this user.
*
- * Not used yet, as of 20200718.
- *
* @param object $course
* @param object $user
* @param object $mod
@@ -280,16 +239,9 @@ function margic_get_post_actions() {
function margic_user_outline($course, $user, $mod, $margic) {
global $DB;
- if ($entry = $DB->get_record("margic_entries", array(
- "userid" => $user->id,
- "margic" => $margic->id
- ))) {
-
- $numwords = count(preg_split("/\w\b/", $entry->text)) - 1;
-
+ if ($count = $DB->count_records("margic_entries", array("userid" => $user->id, "margic" => $margic->id))) {
$result = new stdClass();
- $result->info = get_string("numwords", "", $numwords);
- $result->time = $entry->timemodified;
+ $result->info = $count . ' ' . get_string("entries");
return $result;
}
return null;
@@ -303,7 +255,7 @@ function margic_user_outline($course, $user, $mod, $margic) {
* @param object $mod
* @param object $margic
*/
-function margic_user_complete($course, $user, $mod, $margic) {
+/* function margic_user_complete($course, $user, $mod, $margic) {
global $DB, $OUTPUT;
if ($entry = $DB->get_record("margic_entries", array(
@@ -317,7 +269,7 @@ function margic_user_complete($course, $user, $mod, $margic) {
echo "
';
- }
}
diff --git a/styles.css b/styles.css
index 8044067..13822a3 100644
--- a/styles.css
+++ b/styles.css
@@ -23,14 +23,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
*/
-.path-mod-margic .entrycontent { /* Used in renderer.php. */
- padding: 3px;
-}
-
-.path-mod-margic .picture { /* Used in renderer.php with left. */
- width: 35px;
-}
-
.path-mod-margic .lastedit,
.path-mod-margic .needsedit,
.path-mod-margic .editend {
@@ -54,17 +46,6 @@
padding: 2px;
}
-.path-mod-margic .author { /* Used in renderer.php about line 191. */
- font-size: 1em;
- font-weight: bold;
-}
-
-.path-mod-margic .grade { /* Used in renderer.php about line 200. */
- font-weight: bold;
- font-style: italic;
- text-align: right;
-}
-
.path-mod-margic .entriesheader {
margin-bottom: 10px;
border: 1px solid rgba(0, 0, 0, .125);
diff --git a/version.php b/version.php
index 050b64d..8dd6cb5 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_margic';
-$plugin->release = '1.1.5'; // User-friendly version number.
-$plugin->version = 2022083000; // The current module version (Date: YYYYMMDDXX).
+$plugin->release = '1.1.6'; // User-friendly version number.
+$plugin->version = 2022090100; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2020061507; // Requires Moodle 3.9.
$plugin->maturity = MATURITY_BETA;
From 2d9c2cc0b341661403148f3e8b6d6d0e43bc6850 Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Fri, 2 Sep 2022 15:40:11 +0200
Subject: [PATCH 53/60] fix(entry): revert change for feedback format
---
db/install.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/db/install.xml b/db/install.xml
index efc6e6d..6a56e93 100644
--- a/db/install.xml
+++ b/db/install.xml
@@ -41,7 +41,7 @@
-
+
From 86cc2f919a3a91c894b6546f2b1f923615e90ec6 Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Fri, 2 Sep 2022 20:22:46 +0200
Subject: [PATCH 54/60] fix(version): increase version
---
version.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/version.php b/version.php
index 8dd6cb5..3f9ec3b 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_margic';
-$plugin->release = '1.1.6'; // User-friendly version number.
-$plugin->version = 2022090100; // The current module version (Date: YYYYMMDDXX).
+$plugin->release = '1.2.0'; // User-friendly version number.
+$plugin->version = 2022090200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2020061507; // Requires Moodle 3.9.
$plugin->maturity = MATURITY_BETA;
From d581a9c20aa44450e2616c604e92e756ad13d30b Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Tue, 6 Sep 2022 18:09:39 +0200
Subject: [PATCH 55/60] fix(multiple): fix for backup api, events and js
---
README.md | 4 +-
amd/build/annotations.min.js | 2 +-
amd/build/annotations.min.js.map | 2 +-
amd/build/highlighting.min.js.map | 2 +-
amd/build/match-quote.min.js.map | 2 +-
amd/build/string-match.min.js.map | 2 +-
amd/build/text-range.min.js.map | 2 +-
amd/build/types.min.js.map | 2 +-
amd/src/annotations.js | 42 ++-----
amd/src/highlighting.js | 108 +-----------------
amd/src/match-quote.js | 4 +-
amd/src/string-match.js | 42 +++----
amd/src/text-range.js | 82 +------------
amd/src/types.js | 25 ++--
annotations.php | 25 +++-
backup/moodle2/backup_margic_stepslib.php | 1 +
.../restore_margic_activity_task.class.php | 6 +-
backup/moodle2/restore_margic_stepslib.php | 5 +-
classes/event/annotation_created.php | 99 ++++++++++++++++
classes/event/annotation_deleted.php | 99 ++++++++++++++++
classes/event/annotation_updated.php | 99 ++++++++++++++++
classes/event/course_module_viewed.php | 13 +--
classes/event/download_margic_entries.php | 2 +-
classes/event/entry_created.php | 9 +-
classes/event/entry_updated.php | 9 +-
classes/event/feedback_updated.php | 9 +-
classes/event/invalid_access_attempt.php | 6 +-
classes/local/entrystats.php | 11 +-
classes/local/helper.php | 68 +++++------
classes/search/entry.php | 14 +--
db/log.php | 69 -----------
edit.php | 7 +-
error_summary.php | 12 +-
grade_entry.php | 5 +-
lang/de/margic.php | 5 +-
lang/en/margic.php | 3 +
templates/margic_entry.mustache | 61 +---------
version.php | 2 +-
view.php | 7 ++
39 files changed, 504 insertions(+), 463 deletions(-)
create mode 100644 classes/event/annotation_created.php
create mode 100644 classes/event/annotation_deleted.php
create mode 100644 classes/event/annotation_updated.php
delete mode 100644 db/log.php
diff --git a/README.md b/README.md
index 3fb8d78..efd4dba 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ this program. If not, see .
@copyright 2022 coactum GmbH
-This plugin is based on the official mod_diary Moodle plugin (v. 3.4.1) from AL Rachels () which itself is a fork of the mod_journal plugin (). The original plugin code was widely rewritten in the making of this plugin.
+This plugin is based on the official mod_diary Moodle plugin (v. 3.4.1) from AL Rachels () which itself is a fork of the mod_journal plugin (). The mod_diary plugin code was widely rewritten in the making of this plugin.
The plugin also incorporates JavaScript Code from the Hypothesis project () which is released under the 2-Clause BSD License (), sometimes referred to as the "Simplified BSD License".
@@ -30,7 +30,7 @@ Margics can be used in a meaningful way in language lessons, for example. Studen
Teachers can then view, correct and evaluate these entries on a customizable overview page. For this purpose, they can mark specific text passages and write annotations for them, whereby an error type and a short text can be stored for each annotation. The entire entry can also be graded and provided with textual or acoustic feedback. Participants then have the opportunity to revise their original entry and use the feedback received to improve it.
-The available error types for the annotations can be flexibly adjusted. In an error summary, instructors can also evaluate for each participant how many and which errors they made in a Margic. Finally, it is also possible to export the written entries for further use.
+The available error types for the annotations can be flexibly adjusted. In an error summary, instructors can also evaluate for each participant how many and which errors they made in a Margic. Finally, it is also possible to export or print the entries in a Margic for further use.
Core features of the plugin:
diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js
index 6e20c3e..d43c6ea 100644
--- a/amd/build/annotations.min.js
+++ b/amd/build/annotations.min.js
@@ -5,6 +5,6 @@ define("mod_margic/annotations",["exports","jquery","./highlighting"],(function(
* @module mod_margic/annotations
* @copyright 2022 coactum GmbH
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html((0,_jquery.default)("#annotationpreview-"+annotationid).html()),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;var annotation={target:ranges.map((function(range){return(0,_highlighting.describe)(root,range)})).map((function(selectors){return{selector:selectors}}))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0]),(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotation.exact)}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching annotations")}})}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;var annotation={target:ranges.map((function(range){return(0,_highlighting.describe)(root,range)})).map((function(selectors){return{selector:selectors}}))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0]),(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotation.exact.replaceAll("<","<").replaceAll(">",">"))}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching 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 07646e9..d0e3284 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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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 // console.log('mouseup in originaltext');\n\n removeAllTempHighlights(); // Remove other temporary highlights.\n\n resetForms(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n $('#annotationpreview-temp-' + entry).html(newannotation.target[0].selector[2].exact);\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n // console.log('rangeSelectors');\n // console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n // console.log('target');\n // console.log(target);\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n // console.log(newannotation);\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\n\n $('#annotationpreview-' + annotation.id).html(annotation.exact);\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n // console.log('createAnnotation');\n\n const ranges = [window.getSelection().getRangeAt(0)];\n\n // console.log('createAnnotation ranges');\n // console.log(ranges);\n\n if (ranges.collapsed) {\n return null;\n }\n\n // console.log('createAnnotation -> ROOT');\n // console.log(root);\n\n //const info = await this.getDocumentInfo();\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n // console.log('rangeSelectors');\n // console.log(rangeSelectors);\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n // console.log('target');\n // console.log(target);\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n // console.log('Annotation INFORMATION TO SAVE IN THE DB');\n // console.log(annotation);\n\n anchor(annotation, root);\n\n // console.log('TEMP');\n // console.log(temp);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","annotation","target","map","range","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\n\n // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-' + annotation.id).html(annotation.exact.replaceAll('<', '<').replaceAll('>', '>'));\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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 // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","annotation","target","map","range","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","error","alert"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/build/highlighting.min.js.map b/amd/build/highlighting.min.js.map
index 3d3f591..448e42a 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 // console.log('describe');\n\n for (let type of types) {\n try {\n const anchor = type.fromRange(root, range);\n\n // console.log('type');\n // console.log(type);\n // console.log('anchor');\n // console.log(anchor);\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 // console.log('anchor');\n // console.log('annotation');\n // console.log(annotation);\n\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 // console.log('anchor -> locate');\n // console.log('target');\n // console.log(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 // console.log('anchor -> locate -> after htmlAnchor');\n // console.log('result of htmlAnchor');\n // console.log(range);\n const textRange = TextRange.fromRange(range);\n // console.log('range for anchor');\n // console.log('textRange');\n // console.log(textRange);\n\n anchor = { annotation, target, range: textRange };\n\n // console.log('anchor found');\n // console.log(anchor);\n } catch (err) {\n // console.log('Error in try to find textrange');\n // console.log(err);\n anchor = { annotation, target };\n }\n\n // console.log('anchor at the end of anchor -> locate');\n // console.log(anchor);\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 // console.log('highlight');\n // console.log('highlight resolveAnchor');\n const range = resolveAnchor(anchor);\n // console.log('range');\n // console.log(range);\n\n if (!range) {\n // console.log('no range');\n return;\n }\n\n // console.log('highlight after resolveAnchor');\n // console.log('range');\n // console.log(range);\n\n // console.log('annotation');\n // console.log(annotation);\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 // console.log('highlights after i should have highlighted range');\n // console.log(highlights);\n\n highlights.forEach(h => {\n h._annotation = anchor.annotation;\n });\n anchor.highlights = highlights;\n\n // if (this._focusedAnnotations.has(anchor.annotation.$tag)) {\n // setHighlightsFocused(highlights, true);\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 // console.log('anchors after locate');\n // console.log(anchors);\n\n for (let anchor of anchors) {\n // console.log('before highlighting anchor');\n // console.log('anchor');\n // console.log(anchor);\n highlight(anchor);\n // console.log('after highlighting 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 // console.log('anchor ends');\n // console.log('anchors');\n // console.log(anchors);\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 // console.log('resolveAnchor');\n // console.log('anchor');\n // console.log(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 // console.log('highlightRange');\n // console.log('range');\n // console.log(range);\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 // console.log('querySelector');\n // console.log('anchor');\n // console.log(anchor);\n // console.log('options');\n // console.log(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 // console.log('html.js -> anchor() -> selectors');\n // console.log(selectors);\n\n // console.log('htmlAnchor()');\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 // console.log('maybeAssertQuote');\n // console.log('range');\n // console.log(range);\n // console.log('quote');\n // console.log(quote);\n if (quote?.exact && range.toString() !== quote.exact) {\n throw new Error('quote mismatch');\n } else {\n // console.log('range found!');\n // console.log(range);\n return range;\n }\n };\n\n let queryselector = false;\n\n try {\n if (range) {\n // console.log('html.js -> anchor() range RangeAnchor.fromSelector');\n\n let anchor = RangeAnchor.fromSelector(root, range);\n\n // console.log('anchor');\n // console.log(anchor);\n\n queryselector = querySelector(anchor, options);\n\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for RangeAnchor');\n // console.log(queryselector);\n\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (position) {\n\n // console.log('html.js -> anchor() position TextPositionAnchor.fromSelector');\n\n // console.log('position');\n\n let anchor = TextPositionAnchor.fromSelector(root, position);\n\n queryselector = querySelector(anchor, options);\n if (queryselector) {\n\n // console.log('htmlAnchor queryselector for TextPositionAnchor');\n // console.log(queryselector);\n return queryselector;\n } else {\n return maybeAssertQuote;\n }\n }\n } catch (error) {\n try {\n if (quote) {\n\n // console.log('html.js -> anchor() quote TextQuoteAnchor.fromSelector');\n\n // console.log('quote');\n // console.log('htmlAnchor queryselector for TextQuoteAnchor');\n\n let anchor = TextQuoteAnchor.fromSelector(root, quote);\n\n queryselector = querySelector(anchor, options);\n\n // console.log(queryselector);\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 // console.log('removeHighlights highlights');\n // console.log(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 //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
diff --git a/amd/build/match-quote.min.js.map b/amd/build/match-quote.min.js.map
index a0b4924..ee7ff90 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 */\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 */\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,oBASxBS,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,SAQHQ,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":["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
diff --git a/amd/build/string-match.min.js.map b/amd/build/string-match.min.js.map
index 70f9ee4..6db4109 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 */\n// export interface Match {\n// /** Start offset within the text string of the match. */\n// start: number;\n// /** End offset within the text string of the match. */\n// end: number;\n// /**\n// * The number of differences (insertions, deletions or substitutions) between\n// * the pattern and the approximate match in the text.\n// */\n// errors: number;\n// }\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 findEndFn - Function for finding the end of matches in\n * text.\n * @return 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 */\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 ctx - The pattern context object\n * @param peq - The `peq` array for the current character (`ctx.peq.get(ch)`)\n * @param b - The block level\n * @param hIn - Horizontal input delta ∈ {1,0,-1}\n * @return 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 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 */\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.map b/amd/build/text-range.min.js.map
index 7c2ef40..52e5d67 100644
--- a/amd/build/text-range.min.js.map
+++ b/amd/build/text-range.min.js.map
@@ -1 +1 @@
-{"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 */\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 */\nfunction previousSiblingsTextLength(node) {\n let sibling = node.previousSibling;\n let length = 0;\n\n // console.log('*** START ***');\n // console.log(length, sibling);\n\n while (sibling) {\n length += nodeTextLength(sibling);\n sibling = sibling.previousSibling;\n // console.log(length, sibling);\n }\n\n // if (length == 29) {\n // console.log('LENGTH = 29 !!!!!');\n\n // // console.log('trace');\n // // console.trace();\n // // length = 0;\n // }\n\n // console.log('previousSiblingsTextLength node');\n // console.log(node);\n\n // console.log('previousSiblingsTextLength length');\n // console.log(length);\n\n // console.log('*** END ***');\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 // console.log('text-range.js -> resolveOffsets');\n // console.log('element');\n // console.log(element);\n\n // console.log('offsets');\n // console.log(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 // console.log('nextOffset');\n // console.log(nextOffset);\n\n // console.log('currentNode');\n // console.log(currentNode);\n\n // console.log('offsets');\n // console.log(offsets);\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 // console.log('textNode');\n // console.log(textNode);\n // console.log('offsets');\n // console.log(offsets);\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 && textNode && length === nextOffset) {\n // console.log('boundary case');\n // console.log('nextOffset');\n // console.log(nextOffset);\n\n // console.log('textNode');\n // console.log(textNode);\n\n // console.log('length');\n // console.log(length);\n // console.log('offsets');\n // console.log(offsets);\n\n results.push({ node: textNode, offset: textNode.data.length });\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n // console.log('nextOffset');\n // console.log(nextOffset);\n // console.log('offsets');\n // console.log(offsets);\n throw new RangeError('Offset exceeds text length');\n }\n\n // console.log('results');\n // console.log(results);\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 // console.log('fromPoint');\n // console.log('node');\n // console.log(node);\n\n // console.log('trace');\n // console.trace();\n\n // console.log('offset');\n // console.log(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 */\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 // console.log('TextRange -> toRange()');\n // console.log(this.start.element,this.start.offset,this.end.offset);\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 // console.log('TextRange fromRange');\n // console.log('range');\n // console.log(range);\n // console.log(range.startContainer);\n // console.log(range.startOffset);\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 */\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 */\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 */\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 && 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
diff --git a/amd/build/types.min.js.map b/amd/build/types.min.js.map
index 15f5447..dd7c30c 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 */\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 */\n static fromSelector(root, selector) {\n // console.log('RangeAnchor -> fromSelector before nodeFromXPATH');\n const startContainer = nodeFromXPath(selector.startContainer, root);\n // console.log('after nodeFromXPATH');\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 // console.log('types.js -> toSelector this.range');\n // console.log(this.range);\n\n const normalizedRange = TextRange.fromRange(this.range).toRange();\n\n // console.log('types.js -> toSelector normalizedRange');\n // console.log(normalizedRange);\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 */\n static fromRange(root, range) {\n // console.log('TextPositionAnchor -> fromRange root');\n // console.log(root);\n\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 */\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 */\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 */\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 */\n toRange(options = {}) {\n return this.toPositionAnchor(options).toRange();\n }\n\n /**\n * @param {QuoteMatchOptions} [options]\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 // const match = text.match(this.exact);\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,4DA2Cf,kBACSC,KAAKD,gCAMd,eAOQE,gBAAkBC,qBAAUC,UAAUH,KAAKD,OAAOK,UAKlDC,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,mCAhE7B,SAAiBf,KAAMC,cACd,IAAIF,YAAYC,KAAMC,mCAS/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,mEAwCrCiB,0DAMCvB,KAAMS,MAAOG,mDAClBZ,KAAOA,UACPS,MAAQA,WACRG,IAAMA,oEA6Bb,iBACS,CACLC,KAAM,uBACNJ,MAAOP,KAAKO,MACZG,IAAKV,KAAKU,4BAId,kBACSR,qBAAUoB,YAAYtB,KAAKF,KAAME,KAAKO,MAAOP,KAAKU,KAAKN,qCA/BhE,SAAiBN,KAAMC,WAIfM,UAAYH,qBAAUC,UAAUJ,OAAOwB,WAAWzB,aACjD,IAAIuB,mBACTvB,KACAO,UAAUE,MAAMM,OAChBR,UAAUK,IAAIG,oCAOlB,SAAoBf,KAAMiB,iBACjB,IAAIM,mBAAmBvB,KAAMiB,SAASR,MAAOQ,SAASL,kFA2BpDc,oDAQC1B,KAAM2B,WAAOC,+DAAU,8CAC5B5B,KAAOA,UACP2B,MAAQA,WACRC,QAAUA,qEA+CjB,iBACS,CACLf,KAAM,oBACNc,MAAOzB,KAAKyB,MACZE,OAAQ3B,KAAK0B,QAAQC,OACrBC,OAAQ5B,KAAK0B,QAAQE,+BAOzB,eAAQC,+DAAU,UACT7B,KAAK8B,iBAAiBD,SAASzB,0CAMxC,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,gCAlE9D,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,mCAYrB,SAAoBZ,KAAMiB,cAChBY,OAAmBZ,SAAnBY,OAAQC,OAAWb,SAAXa,cACT,IAAIJ,gBAAgB1B,KAAMiB,SAASU,MAAO,CAAEE,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,cAChBY,OAAmBZ,SAAnBY,OAAQC,OAAWb,SAAXa,cACT,IAAIJ,gBAAgB1B,KAAMiB,SAASU,MAAO,CAACE,OAAAA,OAAQC,OAAAA"}
\ No newline at end of file
diff --git a/amd/src/annotations.js b/amd/src/annotations.js
index ef6f079..883a0c2 100644
--- a/amd/src/annotations.js
+++ b/amd/src/annotations.js
@@ -62,8 +62,6 @@ export const init = (cmid, canmakeannotations, myuserid) => {
if (selectedrange.cloneContents().textContent !== '' && canmakeannotations) {
- // console.log('mouseup in originaltext');
-
removeAllTempHighlights(); // Remove other temporary highlights.
resetForms(); // Reset the annotation forms.
@@ -99,7 +97,9 @@ export const init = (cmid, canmakeannotations, myuserid) => {
$('.annotation-form-' + entry + ' select').val(1);
- $('#annotationpreview-temp-' + entry).html(newannotation.target[0].selector[2].exact);
+ // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).
+ $('#annotationpreview-temp-' + entry).html(
+ newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));
$('.annotationarea-' + entry + ' .annotation-form').show();
$('.annotation-form-' + entry + ' #id_text').focus();
@@ -183,27 +183,20 @@ export const init = (cmid, canmakeannotations, myuserid) => {
{type: "TextQuoteSelector", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}
]];
- // console.log('rangeSelectors');
- // console.log(rangeSelectors);
-
const target = rangeSelectors.map(selectors => ({
selector: selectors,
}));
- // console.log('target');
- // console.log(target);
-
/** @type {AnnotationData} */
const newannotation = {
annotation: annotation,
target: target,
};
- // console.log(newannotation);
-
anchor(newannotation, $("#entry-" + annotation.entry)[0]);
- $('#annotationpreview-' + annotation.id).html(annotation.exact);
+ // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).
+ $('#annotationpreview-' + annotation.id).html(annotation.exact.replaceAll('<', '<').replaceAll('>', '>'));
}
}
@@ -244,7 +237,9 @@ export const init = (cmid, canmakeannotations, myuserid) => {
$('.annotation-form-' + entry + ' select').val(annotations[annotationid].type);
- $('#annotationpreview-temp-' + entry).html($('#annotationpreview-' + annotationid).html());
+ // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).
+ $('#annotationpreview-temp-' + entry).html(
+ annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));
$('#annotationpreview-temp-' + entry).css('border-color', '#' + annotations[annotationid].color);
$('.annotationarea-' + entry + ' .annotation-form').insertBefore('.annotation-box-' + annotationid);
@@ -282,45 +277,24 @@ export const init = (cmid, canmakeannotations, myuserid) => {
* @return {object} - The new annotation
*/
function createAnnotation(root) {
- // console.log('createAnnotation');
-
const ranges = [window.getSelection().getRangeAt(0)];
- // console.log('createAnnotation ranges');
- // console.log(ranges);
-
if (ranges.collapsed) {
return null;
}
- // console.log('createAnnotation -> ROOT');
- // console.log(root);
-
- //const info = await this.getDocumentInfo();
const rangeSelectors = ranges.map(range => describe(root, range));
- // console.log('rangeSelectors');
- // console.log(rangeSelectors);
-
const target = rangeSelectors.map(selectors => ({
selector: selectors,
}));
- // console.log('target');
- // console.log(target);
-
/** @type {AnnotationData} */
const annotation = {
target,
};
- // console.log('Annotation INFORMATION TO SAVE IN THE DB');
- // console.log(annotation);
-
anchor(annotation, root);
- // console.log('TEMP');
- // console.log(temp);
-
return annotation;
}
\ No newline at end of file
diff --git a/amd/src/highlighting.js b/amd/src/highlighting.js
index 025c58d..4daab7d 100644
--- a/amd/src/highlighting.js
+++ b/amd/src/highlighting.js
@@ -21,17 +21,10 @@ export function describe(root, range) {
const types = [RangeAnchor, TextPositionAnchor, TextQuoteAnchor];
const result = [];
- // console.log('describe');
-
for (let type of types) {
try {
const anchor = type.fromRange(root, range);
- // console.log('type');
- // console.log(type);
- // console.log('anchor');
- // console.log(anchor);
-
result.push(anchor.toSelector());
} catch (error) {
continue;
@@ -54,10 +47,6 @@ export function describe(root, range) {
* @return {obj} achor object
*/
export function anchor(annotation, root) {
- // console.log('anchor');
- // console.log('annotation');
- // console.log(annotation);
-
/**
* Resolve an annotation's selectors to a concrete range.
*
@@ -66,10 +55,6 @@ export function describe(root, range) {
*/
const locate = target => {
- // console.log('anchor -> locate');
- // console.log('target');
- // console.log(target);
-
// Only annotations with an associated quote can currently be anchored.
// This is because the quote is used to verify anchoring with other selector
// types.
@@ -88,26 +73,17 @@ export function describe(root, range) {
// a `Range` later. The `TextRange` representation allows for highlights
// to be inserted during anchoring other annotations without "breaking"
// this anchor.
- // console.log('anchor -> locate -> after htmlAnchor');
- // console.log('result of htmlAnchor');
- // console.log(range);
+
+
const textRange = TextRange.fromRange(range);
- // console.log('range for anchor');
- // console.log('textRange');
- // console.log(textRange);
anchor = { annotation, target, range: textRange };
- // console.log('anchor found');
- // console.log(anchor);
} catch (err) {
- // console.log('Error in try to find textrange');
- // console.log(err);
+
anchor = { annotation, target };
}
- // console.log('anchor at the end of anchor -> locate');
- // console.log(anchor);
return anchor;
};
@@ -117,24 +93,13 @@ export function describe(root, range) {
* @param {Anchor} anchor
*/
const highlight = anchor => {
- // console.log('highlight');
- // console.log('highlight resolveAnchor');
+
const range = resolveAnchor(anchor);
- // console.log('range');
- // console.log(range);
if (!range) {
- // console.log('no range');
return;
}
- // console.log('highlight after resolveAnchor');
- // console.log('range');
- // console.log(range);
-
- // console.log('annotation');
- // console.log(annotation);
-
let highlights = [];
if (annotation.annotation) {
@@ -143,17 +108,11 @@ export function describe(root, range) {
highlights = highlightRange(range, false, 'annotated_temp');
}
- // console.log('highlights after i should have highlighted range');
- // console.log(highlights);
-
highlights.forEach(h => {
h._annotation = anchor.annotation;
});
anchor.highlights = highlights;
- // if (this._focusedAnnotations.has(anchor.annotation.$tag)) {
- // setHighlightsFocused(highlights, true);
- // }
};
// Remove existing anchors for this annotation.
@@ -164,15 +123,10 @@ export function describe(root, range) {
annotation.target = [];
}
const anchors = annotation.target.map(locate);
- // console.log('anchors after locate');
- // console.log(anchors);
for (let anchor of anchors) {
- // console.log('before highlighting anchor');
- // console.log('anchor');
- // console.log(anchor);
+
highlight(anchor);
- // console.log('after highlighting anchor');
}
// Set flag indicating whether anchoring succeeded. For each target,
@@ -182,9 +136,6 @@ export function describe(root, range) {
anchors.length > 0 &&
anchors.every(anchor => anchor.target.selector && !anchor.range);
- // console.log('anchor ends');
- // console.log('anchors');
- // console.log(anchors);
return anchors;
}
@@ -198,9 +149,6 @@ export function describe(root, range) {
* @return {Range|null}
*/
function resolveAnchor(anchor) {
- // console.log('resolveAnchor');
- // console.log('anchor');
- // console.log(anchor);
if (!anchor.range) {
return null;
@@ -225,9 +173,6 @@ function resolveAnchor(anchor) {
* @return {HighlightElement[]} - Elements wrapping text in `normedRange` to add a highlight effect
*/
function highlightRange(range, annotationid = false, cssClass = 'annotated', color = 'FFFF00') {
- // console.log('highlightRange');
- // console.log('range');
- // console.log(range);
const textNodes = wholeTextNodesInRange(range);
@@ -376,11 +321,6 @@ function isNodeInRange(range, node) {
* @return {obj} - range
*/
function querySelector(anchor, options = {}) {
- // console.log('querySelector');
- // console.log('anchor');
- // console.log(anchor);
- // console.log('options');
- // console.log(options);
return anchor.toRange(options);
}
@@ -402,11 +342,6 @@ function isNodeInRange(range, node) {
let quote = null;
let range = null;
- // console.log('html.js -> anchor() -> selectors');
- // console.log(selectors);
-
- // console.log('htmlAnchor()');
-
// Collect all the selectors
for (let selector of selectors) {
switch (selector.type) {
@@ -429,16 +364,10 @@ function isNodeInRange(range, node) {
* @return {Range} range
*/
const maybeAssertQuote = range => {
- // console.log('maybeAssertQuote');
- // console.log('range');
- // console.log(range);
- // console.log('quote');
- // console.log(quote);
+
if (quote?.exact && range.toString() !== quote.exact) {
throw new Error('quote mismatch');
} else {
- // console.log('range found!');
- // console.log(range);
return range;
}
};
@@ -447,20 +376,12 @@ function isNodeInRange(range, node) {
try {
if (range) {
- // console.log('html.js -> anchor() range RangeAnchor.fromSelector');
let anchor = RangeAnchor.fromSelector(root, range);
- // console.log('anchor');
- // console.log(anchor);
-
queryselector = querySelector(anchor, options);
if (queryselector) {
-
- // console.log('htmlAnchor queryselector for RangeAnchor');
- // console.log(queryselector);
-
return queryselector;
} else {
return maybeAssertQuote;
@@ -470,17 +391,10 @@ function isNodeInRange(range, node) {
try {
if (position) {
- // console.log('html.js -> anchor() position TextPositionAnchor.fromSelector');
-
- // console.log('position');
-
let anchor = TextPositionAnchor.fromSelector(root, position);
queryselector = querySelector(anchor, options);
if (queryselector) {
-
- // console.log('htmlAnchor queryselector for TextPositionAnchor');
- // console.log(queryselector);
return queryselector;
} else {
return maybeAssertQuote;
@@ -490,17 +404,10 @@ function isNodeInRange(range, node) {
try {
if (quote) {
- // console.log('html.js -> anchor() quote TextQuoteAnchor.fromSelector');
-
- // console.log('quote');
- // console.log('htmlAnchor queryselector for TextQuoteAnchor');
-
let anchor = TextQuoteAnchor.fromSelector(root, quote);
queryselector = querySelector(anchor, options);
- // console.log(queryselector);
-
return queryselector;
}
} catch (error) {
@@ -527,9 +434,6 @@ function isNodeInRange(range, node) {
*/
function removeHighlights(highlights) {
- // console.log('removeHighlights highlights');
- // console.log(highlights);
-
for (var i = 0; i < highlights.length; i++) {
if (highlights[i].parentNode) {
//var pn = highlights[i].parentNode;
diff --git a/amd/src/match-quote.js b/amd/src/match-quote.js
index 9830d08..610fe4f 100644
--- a/amd/src/match-quote.js
+++ b/amd/src/match-quote.js
@@ -51,6 +51,7 @@ function search(text, str, maxErrors) {
*
* @param {string} text
* @param {string} str
+ * @return {int}
*/
function textMatchScore(text, str) {
// `search` will return no matches if either the text or pattern is empty,
@@ -62,7 +63,7 @@ function textMatchScore(text, str) {
const matches = search(text, str, str.length);
- // prettier-ignore
+ // Prettier-ignore.
return 1 - (matches[0].errors / str.length);
}
@@ -108,6 +109,7 @@ export function matchQuote(text, quote, context = {}) {
* Compute a score between 0 and 1.0 for a match candidate.
*
* @param {StringMatch} match
+ * @return {int}
*/
const scoreMatch = match => {
const quoteWeight = 50; // Similarity of matched text to quote.
diff --git a/amd/src/string-match.js b/amd/src/string-match.js
index 8d8c2b4..7765be8 100644
--- a/amd/src/string-match.js
+++ b/amd/src/string-match.js
@@ -7,19 +7,9 @@
/**
* Represents a match returned by a call to `search`.
+ * @param {string} s - Document text to search
+ * @return {string}
*/
-// export interface Match {
-// /** Start offset within the text string of the match. */
-// start: number;
-// /** End offset within the text string of the match. */
-// end: number;
-// /**
-// * The number of differences (insertions, deletions or substitutions) between
-// * the pattern and the approximate match in the text.
-// */
-// errors: number;
-// }
-
function reverse(s) {
return s.split("").reverse().join("");
}
@@ -28,9 +18,10 @@
* Given the ends of approximate matches for `pattern` in `text`, find
* the start of the matches.
*
- * @param findEndFn - Function for finding the end of matches in
- * text.
- * @return Matches with the `start` property set.
+ * @param {string} text
+ * @param {string} pattern
+ * @param {array} matches
+ * @return {obj} Matches with the `start` property set.
*/
function findMatchStarts(text, pattern, matches) {
const patRev = reverse(pattern);
@@ -83,6 +74,8 @@
* This should get inlined into `advanceBlock` below by the JIT.
*
* Adapted from https://stackoverflow.com/a/3912218/434243
+ * @param {int} n
+ * @return {bool}
*/
function oneIfNotZero(n) {
return ((n | -n) >> 31) & 1;
@@ -94,11 +87,11 @@
* From Fig 8. on p. 408 of [1], additionally optimized to replace conditional
* checks with bitwise operations as per Section 4.2.3 of [2].
*
- * @param ctx - The pattern context object
- * @param peq - The `peq` array for the current character (`ctx.peq.get(ch)`)
- * @param b - The block level
- * @param hIn - Horizontal input delta ∈ {1,0,-1}
- * @return Horizontal output delta ∈ {1,0,-1}
+ * @param {obj} ctx - The pattern context object
+ * @param {array} peq - The `peq` array for the current character (`ctx.peq.get(ch)`)
+ * @param {int} b - The block level
+ * @param {obj} hIn - Horizontal input delta ∈ {1,0,-1}
+ * @return {obj} Horizontal output delta ∈ {1,0,-1}
*/
function advanceBlock(ctx, peq, b, hIn) {
let pV = ctx.P[b];
@@ -141,6 +134,11 @@
* with error counts <= maxErrors are discarded.
*
* This is the block-based search algorithm from Fig. 9 on p.410 of [1].
+ *
+ * @param {string} text
+ * @param {string} pattern
+ * @param {array} maxErrors
+ * @return {obj} Matches with the `start` property set.
*/
function findMatchEnds(text, pattern, maxErrors) {
if (pattern.length === 0) {
@@ -326,6 +324,10 @@
*
* Returns the start, and end positions and error counts for each lowest-cost
* match. Only the "best" matches are returned.
+ * @param {string} text
+ * @param {string} pattern
+ * @param {array} maxErrors
+ * @return {obj} Matches with the `start` property set.
*/
export default function search(
text,
diff --git a/amd/src/text-range.js b/amd/src/text-range.js
index b2e2ada..1be3b3d 100644
--- a/amd/src/text-range.js
+++ b/amd/src/text-range.js
@@ -33,31 +33,11 @@ function previousSiblingsTextLength(node) {
let sibling = node.previousSibling;
let length = 0;
- // console.log('*** START ***');
- // console.log(length, sibling);
-
while (sibling) {
length += nodeTextLength(sibling);
sibling = sibling.previousSibling;
- // console.log(length, sibling);
}
- // if (length == 29) {
- // console.log('LENGTH = 29 !!!!!');
-
- // // console.log('trace');
- // // console.trace();
- // // length = 0;
- // }
-
- // console.log('previousSiblingsTextLength node');
- // console.log(node);
-
- // console.log('previousSiblingsTextLength length');
- // console.log(length);
-
- // console.log('*** END ***');
-
return length;
}
@@ -71,13 +51,6 @@ function previousSiblingsTextLength(node) {
*/
function resolveOffsets(element, ...offsets) {
- // console.log('text-range.js -> resolveOffsets');
- // console.log('element');
- // console.log(element);
-
- // console.log('offsets');
- // console.log(offsets);
-
let nextOffset = offsets.shift();
const nodeIter = /** @type {Document} */ (
element.ownerDocument
@@ -88,23 +61,11 @@ function resolveOffsets(element, ...offsets) {
let textNode;
let length = 0;
- // console.log('nextOffset');
- // console.log(nextOffset);
-
- // console.log('currentNode');
- // console.log(currentNode);
-
- // console.log('offsets');
- // console.log(offsets);
-
// Find the text node containing the `nextOffset`th character from the start
// of `element`.
while (nextOffset !== undefined && currentNode) {
textNode = /** @type {Text} */ (currentNode);
- // console.log('textNode');
- // console.log(textNode);
- // console.log('offsets');
- // console.log(offsets);
+
if (length + textNode.data.length > nextOffset) {
results.push({ node: textNode, offset: nextOffset - length });
nextOffset = offsets.shift();
@@ -116,33 +77,14 @@ function resolveOffsets(element, ...offsets) {
// Boundary case.
while (nextOffset !== undefined && textNode && length === nextOffset) {
- // console.log('boundary case');
- // console.log('nextOffset');
- // console.log(nextOffset);
-
- // console.log('textNode');
- // console.log(textNode);
-
- // console.log('length');
- // console.log(length);
- // console.log('offsets');
- // console.log(offsets);
-
results.push({ node: textNode, offset: textNode.data.length });
nextOffset = offsets.shift();
}
if (nextOffset !== undefined) {
- // console.log('nextOffset');
- // console.log(nextOffset);
- // console.log('offsets');
- // console.log(offsets);
throw new RangeError('Offset exceeds text length');
}
- // console.log('results');
- // console.log(results);
-
return results;
}
@@ -266,15 +208,6 @@ export class TextPosition {
* @return {TextPosition}
*/
static fromPoint(node, offset) {
- // console.log('fromPoint');
- // console.log('node');
- // console.log(node);
-
- // console.log('trace');
- // console.trace();
-
- // console.log('offset');
- // console.log(offset);
switch (node.nodeType) {
case Node.TEXT_NODE: {
@@ -334,6 +267,7 @@ export class TextRange {
* given ancestor. See `TextPosition.relativeTo`.
*
* @param {Element} element
+ * @return {Range}
*/
relativeTo(element) {
return new TextRange(
@@ -362,16 +296,14 @@ export class TextRange {
this.start.offset <= this.end.offset
) {
// Fast path for start and end points in same element.
- // console.log('TextRange -> toRange()');
- // console.log(this.start.element,this.start.offset,this.end.offset);
[start, end] = resolveOffsets(
this.start.element,
this.start.offset,
this.end.offset
);
} else {
- start = this.start.resolve({ direction: RESOLVE_FORWARDS });
- end = this.end.resolve({ direction: RESOLVE_BACKWARDS });
+ start = this.start.resolve({direction: RESOLVE_FORWARDS});
+ end = this.end.resolve({direction: RESOLVE_BACKWARDS});
}
const range = new Range();
@@ -387,11 +319,6 @@ export class TextRange {
* @return {TextRange}
*/
static fromRange(range) {
- // console.log('TextRange fromRange');
- // console.log('range');
- // console.log(range);
- // console.log(range.startContainer);
- // console.log(range.startOffset);
const start = TextPosition.fromPoint(
range.startContainer,
range.startOffset
@@ -406,6 +333,7 @@ export class TextRange {
* @param {Element} root
* @param {number} start
* @param {number} end
+ * @return {Range}
*/
static fromOffsets(root, start, end) {
return new TextRange(
diff --git a/amd/src/types.js b/amd/src/types.js
index 645ab9c..547fef4 100644
--- a/amd/src/types.js
+++ b/amd/src/types.js
@@ -38,6 +38,7 @@ export class RangeAnchor {
/**
* @param {Node} root - A root element from which to anchor.
* @param {Range} range - A range describing the anchor.
+ * @return {RangeAnchor}
*/
static fromRange(root, range) {
return new RangeAnchor(root, range);
@@ -48,11 +49,12 @@ export class RangeAnchor {
*
* @param {Element} root - A root element from which to anchor.
* @param {RangeSelector} selector
+ * @return {RangeAnchor}
*/
static fromSelector(root, selector) {
- // console.log('RangeAnchor -> fromSelector before nodeFromXPATH');
+
const startContainer = nodeFromXPath(selector.startContainer, root);
- // console.log('after nodeFromXPATH');
+
if (!startContainer) {
throw new Error('Failed to resolve startContainer XPath');
}
@@ -86,14 +88,8 @@ export class RangeAnchor {
// "Shrink" the range so that it tightly wraps its text. This ensures more
// predictable output for a given text selection.
- // console.log('types.js -> toSelector this.range');
- // console.log(this.range);
-
const normalizedRange = TextRange.fromRange(this.range).toRange();
- // console.log('types.js -> toSelector normalizedRange');
- // console.log(normalizedRange);
-
const textRange = TextRange.fromRange(normalizedRange);
const startContainer = xpathFromNode(textRange.start.element, this.root);
const endContainer = xpathFromNode(textRange.end.element, this.root);
@@ -126,11 +122,9 @@ export class TextPositionAnchor {
/**
* @param {Element} root
* @param {Range} range
+ * @return {TextPositionAnchor}
*/
static fromRange(root, range) {
- // console.log('TextPositionAnchor -> fromRange root');
- // console.log(root);
-
const textRange = TextRange.fromRange(range).relativeTo(root);
return new TextPositionAnchor(
root,
@@ -141,6 +135,7 @@ export class TextPositionAnchor {
/**
* @param {Element} root
* @param {TextPositionSelector} selector
+ * @return {TextPositionAnchor}
*/
static fromSelector(root, selector) {
return new TextPositionAnchor(root, selector.start, selector.end);
@@ -191,6 +186,7 @@ export class TextQuoteAnchor {
*
* @param {Element} root
* @param {Range} range
+ * @return {TextQuoteAnchor}
*/
static fromRange(root, range) {
const text = /** @type {string} */ (root.textContent);
@@ -219,10 +215,11 @@ export class TextQuoteAnchor {
/**
* @param {Element} root
* @param {TextQuoteSelector} selector
+ * @return {TextQuoteAnchor}
*/
static fromSelector(root, selector) {
const { prefix, suffix } = selector;
- return new TextQuoteAnchor(root, selector.exact, { prefix, suffix });
+ return new TextQuoteAnchor(root, selector.exact, {prefix, suffix});
}
/**
@@ -239,6 +236,7 @@ export class TextQuoteAnchor {
/**
* @param {QuoteMatchOptions} [options]
+ * @return {TextQuoteAnchor}
*/
toRange(options = {}) {
return this.toPositionAnchor(options).toRange();
@@ -246,6 +244,7 @@ export class TextQuoteAnchor {
/**
* @param {QuoteMatchOptions} [options]
+ * @return {TextPositionAnchor}
*/
toPositionAnchor(options = {}) {
const text = /** @type {string} */ (this.root.textContent);
@@ -253,7 +252,7 @@ export class TextQuoteAnchor {
...this.context,
hint: options.hint,
});
- // const match = text.match(this.exact);
+
if (!match) {
throw new Error('Quote not found');
}
diff --git a/annotations.php b/annotations.php
index 099112c..3094a12 100644
--- a/annotations.php
+++ b/annotations.php
@@ -97,6 +97,14 @@
if ($DB->record_exists('margic_annotations', array('id' => $deleteannotation, 'margic' => $moduleinstance->id, 'userid' => $USER->id))) {
$DB->delete_records('margic_annotations', array('id' => $deleteannotation, 'margic' => $moduleinstance->id, 'userid' => $USER->id));
+ // Trigger module annotation deleted event.
+ $event = \mod_margic\event\annotation_deleted::create(array(
+ 'objectid' => $deleteannotation,
+ 'context' => $context
+ ));
+
+ $event->trigger();
+
redirect($redirecturl, get_string('annotationdeleted', 'mod_margic'), null, notification::NOTIFY_SUCCESS);
} else {
redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR);
@@ -132,6 +140,14 @@
$DB->update_record('margic_annotations', $annotation);
+ // Trigger module annotation updated event.
+ $event = \mod_margic\event\annotation_updated::create(array(
+ 'objectid' => $fromform->annotationid,
+ 'context' => $context
+ ));
+
+ $event->trigger();
+
redirect($redirecturl, get_string('annotationedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS);
} else if ((!isset($fromform->annotationid) || $fromform->annotationid === 0) && isset($fromform->text)) { // New annotation.
@@ -168,7 +184,14 @@
$annotation->suffix = format_text($fromform->suffix, 2, array('para' => false));;
$annotation->text = format_text($fromform->text, 2, array('para' => false));
- $DB->insert_record('margic_annotations', $annotation);
+ $newid = $DB->insert_record('margic_annotations', $annotation);
+
+ // Trigger module annotation created event.
+ $event = \mod_margic\event\annotation_created::create(array(
+ 'objectid' => $newid,
+ 'context' => $context
+ ));
+ $event->trigger();
redirect($redirecturl, get_string('annotationadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS);
} else {
diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php
index dc023f8..9583c4d 100644
--- a/backup/moodle2/backup_margic_stepslib.php
+++ b/backup/moodle2/backup_margic_stepslib.php
@@ -111,6 +111,7 @@ protected function define_structure() {
}
// Define file annotations.
+ // component, filearea, elementname.
$margic->annotate_files('mod_margic', 'intro', null); // This file area has no itemid.
$entry->annotate_files('mod_margic', 'entry', 'id');
$entry->annotate_files('mod_margic', 'feedback', 'id');
diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php
index 36c9ef9..2a29211 100644
--- a/backup/moodle2/restore_margic_activity_task.class.php
+++ b/backup/moodle2/restore_margic_activity_task.class.php
@@ -55,7 +55,8 @@ protected function define_my_steps() {
public static function define_decode_contents() {
$contents = array();
- // Define the contents (files in textareas).
+ // Define the contents (files).
+ // tablename, array(field1, field 2), $mapping.
$contents[] = new restore_decode_content('margic', array('intro'), 'margic');
$contents[] = new restore_decode_content('margic_entries', array('text', 'feedback'), 'margic_entry');
@@ -95,6 +96,9 @@ public static function define_restore_log_rules() {
$rules[] = new restore_log_rule('margic', 'view', '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', 'add annotation', 'view.php?id={course_module}', '{margic}');
+ $rules[] = new restore_log_rule('margic', 'update annotation', 'view.php?id={course_module}', '{margic}');
+ $rules[] = new restore_log_rule('margic', 'delete annotation', 'view.php?id={course_module}', '{margic}');
$rules[] = new restore_log_rule('margic', 'update feedback', 'view.php?id={course_module}', '{margic}');
$rules[] = new restore_log_rule('margic', 'download entries', 'view.php?id={course_module}', '{margic}');
$rules[] = new restore_log_rule('margic', 'invalid access attempt', 'edit.php?id={course_module}', '{margic}');
diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php
index 0589fe8..91057f5 100644
--- a/backup/moodle2/restore_margic_stepslib.php
+++ b/backup/moodle2/restore_margic_stepslib.php
@@ -116,7 +116,7 @@ protected function process_margic_entry($data) {
}
$newitemid = $DB->insert_record('margic_entries', $data);
- $this->set_mapping('margic_entry', $oldid, $newitemid);
+ $this->set_mapping('margic_entry', $oldid, $newitemid, true); // The true parameter is necessary for file handling
}
/**
@@ -195,7 +195,8 @@ protected function after_execute() {
// Add margic related files, no need to match by itemname (just internally handled context).
$this->add_related_files('mod_margic', 'intro', null);
- $this->add_related_files('mod_margic', 'text', 'margic_entry');
+ // Component, filearea, mapping.
+ $this->add_related_files('mod_margic', 'entry', 'margic_entry');
$this->add_related_files('mod_margic', 'feedback', 'margic_entry');
}
diff --git a/classes/event/annotation_created.php b/classes/event/annotation_created.php
new file mode 100644
index 0000000..33ba435
--- /dev/null
+++ b/classes/event/annotation_created.php
@@ -0,0 +1,99 @@
+.
+
+/**
+ * The mod_margic annotation created event.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_margic\event;
+
+/**
+ * The mod_margic annotation created class.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class annotation_created extends \core\event\base {
+
+ /**
+ * Init method.
+ */
+ protected function init() {
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_TEACHING;
+ $this->data['objecttable'] = 'margic';
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventannotationcreated', 'mod_margic');
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with the id '$this->userid' has created the annotation with the id '$this->objectid' for the margic activity with " .
+ "the course module id '$this->contextinstanceid'";
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/mod/margic/view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ }
+
+ /**
+ * replace add_to_log() statement.
+ *
+ * @return array of parameters to be passed to legacy add_to_log() function.
+ */
+ protected function get_legacy_logdata() {
+ $url = new \moodle_url('view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ return array(
+ $this->courseid,
+ 'margic',
+ 'add annotation',
+ $url->out(),
+ $this->objectid,
+ $this->contextinstanceid
+ );
+ }
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_annotations', 'restore' => 'margic_annotation');
+ }
+}
diff --git a/classes/event/annotation_deleted.php b/classes/event/annotation_deleted.php
new file mode 100644
index 0000000..b868ac4
--- /dev/null
+++ b/classes/event/annotation_deleted.php
@@ -0,0 +1,99 @@
+.
+
+/**
+ * The mod_margic annotation deleted event.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_margic\event;
+
+/**
+ * The mod_margic annotation deleted class.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class annotation_deleted extends \core\event\base {
+
+ /**
+ * Init method.
+ */
+ protected function init() {
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_TEACHING;
+ $this->data['objecttable'] = 'margic';
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventannotationdeleted', 'mod_margic');
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with the id '$this->userid' has deleted the annotation with the id '$this->objectid' for the margic activity with the course module id
+ '$this->contextinstanceid'";
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/mod/margic/view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ }
+
+ /**
+ * replace add_to_log() statement.
+ *
+ * @return array of parameters to be passed to legacy add_to_log() function.
+ */
+ protected function get_legacy_logdata() {
+ $url = new \moodle_url('view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ return array(
+ $this->courseid,
+ 'margic',
+ 'delete annotation',
+ $url->out(),
+ $this->objectid,
+ $this->contextinstanceid
+ );
+ }
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_annotations', 'restore' => 'margic_annotation');
+ }
+}
diff --git a/classes/event/annotation_updated.php b/classes/event/annotation_updated.php
new file mode 100644
index 0000000..e48b876
--- /dev/null
+++ b/classes/event/annotation_updated.php
@@ -0,0 +1,99 @@
+.
+
+/**
+ * The mod_margic annotation updated event.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace mod_margic\event;
+
+/**
+ * The mod_margic annotation updated class.
+ *
+ * @package mod_margic
+ * @copyright 2022 coactum GmbH
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class annotation_updated extends \core\event\base {
+
+ /**
+ * Init method.
+ */
+ protected function init() {
+ $this->data['crud'] = 'u';
+ $this->data['edulevel'] = self::LEVEL_TEACHING;
+ $this->data['objecttable'] = 'margic';
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventannotationupdated', 'mod_margic');
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with the id '$this->userid' has updated the annotation with the id '$this->objectid' for the margic activity with the course module id
+ '$this->contextinstanceid'";
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ return new \moodle_url('/mod/margic/view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ }
+
+ /**
+ * replace add_to_log() statement.
+ *
+ * @return array of parameters to be passed to legacy add_to_log() function.
+ */
+ protected function get_legacy_logdata() {
+ $url = new \moodle_url('view.php', array(
+ 'id' => $this->contextinstanceid
+ ));
+ return array(
+ $this->courseid,
+ 'margic',
+ 'update annotation',
+ $url->out(),
+ $this->objectid,
+ $this->contextinstanceid
+ );
+ }
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_annotations', 'restore' => 'margic_annotation');
+ }
+}
diff --git a/classes/event/course_module_viewed.php b/classes/event/course_module_viewed.php
index ed4e31b..c8a4d75 100644
--- a/classes/event/course_module_viewed.php
+++ b/classes/event/course_module_viewed.php
@@ -43,17 +43,6 @@ protected function init() {
$this->data['objecttable'] = 'margic';
}
- /**
- * Get URL related to the action
- *
- * @return \moodle_url
- */
- public function get_url() {
- return new \moodle_url('/mod/margic/view.php', array(
- 'id' => $this->objectid
- ));
- }
-
/**
* Return the legacy event log data.
*
@@ -74,7 +63,7 @@ protected function get_legacy_logdata() {
}
/**
- * Get objectid mapping
+ * Get objectid mapping for restore.
*/
public static function get_objectid_mapping() {
return array('db' => 'margic', 'restore' => 'margic');
diff --git a/classes/event/download_margic_entries.php b/classes/event/download_margic_entries.php
index a8c6c1b..1747ed1 100644
--- a/classes/event/download_margic_entries.php
+++ b/classes/event/download_margic_entries.php
@@ -36,7 +36,7 @@ class download_margic_entries extends \core\event\base {
* Init method.
*/
protected function init() {
- $this->data['crud'] = 'u';
+ $this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
$this->data['objecttable'] = 'margic';
}
diff --git a/classes/event/entry_created.php b/classes/event/entry_created.php
index 78b3efb..02545cc 100644
--- a/classes/event/entry_created.php
+++ b/classes/event/entry_created.php
@@ -56,7 +56,7 @@ public static function get_name() {
* @return string
*/
public function get_description() {
- return "The user with id '$this->userid' has created an entry for the margic activity with " .
+ return "The user with the id '$this->userid' has created the entry with the id '$this->objectid' for the margic activity with " .
"the course module id '$this->contextinstanceid'";
}
@@ -89,4 +89,11 @@ protected function get_legacy_logdata() {
$this->contextinstanceid
);
}
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_entries', 'restore' => 'margic_entry');
+ }
}
diff --git a/classes/event/entry_updated.php b/classes/event/entry_updated.php
index e850a36..067dbd6 100644
--- a/classes/event/entry_updated.php
+++ b/classes/event/entry_updated.php
@@ -56,7 +56,7 @@ public static function get_name() {
* @return string
*/
public function get_description() {
- return "The user with id '$this->userid' has updated an entry for the margic activity with the course module id
+ return "The user with the id '$this->userid' has updated the entry with the id '$this->objectid' for the margic activity with the course module id
'$this->contextinstanceid'";
}
@@ -89,4 +89,11 @@ protected function get_legacy_logdata() {
$this->contextinstanceid
);
}
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_entries', 'restore' => 'margic_entry');
+ }
}
diff --git a/classes/event/feedback_updated.php b/classes/event/feedback_updated.php
index 6f31cd4..9a8dfed 100644
--- a/classes/event/feedback_updated.php
+++ b/classes/event/feedback_updated.php
@@ -56,7 +56,7 @@ public static function get_name() {
* @return string
*/
public function get_description() {
- return "The user with id '$this->userid' has updated the feedback for an entry for the margic activity with the course module id
+ return "The user with id '$this->userid' has updated the feedback for the entry with the id '$this->objectid' for the margic activity with the course module id
'$this->contextinstanceid'";
}
@@ -89,4 +89,11 @@ protected function get_legacy_logdata() {
$this->contextinstanceid
);
}
+
+ /**
+ * Get objectid mapping for restore.
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'margic_entries', 'restore' => 'margic_entry');
+ }
}
diff --git a/classes/event/invalid_access_attempt.php b/classes/event/invalid_access_attempt.php
index 821bb35..40504a4 100644
--- a/classes/event/invalid_access_attempt.php
+++ b/classes/event/invalid_access_attempt.php
@@ -39,7 +39,7 @@ class invalid_access_attempt extends \core\event\base {
* Init method.
*/
protected function init() {
- $this->data['crud'] = 'd';
+ $this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_TEACHING;
$this->data['objecttable'] = 'margic';
}
@@ -59,8 +59,8 @@ public static function get_name() {
* @return string
*/
public function get_description() {
- return "The user with id '$this->userid' attempted illegal access to 'margic' page
- '{$this->other['file']}' while in the course with id '$this->contextinstanceid'.";
+ return "The user with the id '$this->userid' attempted illegal access to the page
+ '{$this->other['file']}' in the margic activity with the course module id '$this->contextinstanceid'.";
}
/**
diff --git a/classes/local/entrystats.php b/classes/local/entrystats.php
index 3f4c94d..80bc8a5 100644
--- a/classes/local/entrystats.php
+++ b/classes/local/entrystats.php
@@ -54,7 +54,16 @@ public static function get_entry_stats($entrytext, $entrytimecreated) {
$entrystats['uniquewords'] = self::get_stats_uniquewords($cleantext);
$entrystats['spaces'] = self::get_stats_spaces($cleantext) - $replacementspacescount;
$entrystats['charswithoutspaces'] = $entrystats['chars'] - $entrystats['spaces'];
- $entrystats['datediff'] = date_diff(new \DateTime(date('Y-m-d G:i:s', time())), new \DateTime(date('Y-m-d G:i:s', $entrytimecreated)));
+
+ $timenow = new \DateTime(date('Y-m-d G:i:s', time()));
+ $timeentrycreated = new \DateTime(date('Y-m-d G:i:s', $entrytimecreated));
+
+ if ($timenow >= $timeentrycreated) {
+ $entrystats['datediff'] = date_diff($timenow, $timeentrycreated);
+ } else {
+ $entrystats['datediff'] = false;
+ }
+
return $entrystats;
}
diff --git a/classes/local/helper.php b/classes/local/helper.php
index 8441a08..8c697e2 100644
--- a/classes/local/helper.php
+++ b/classes/local/helper.php
@@ -192,18 +192,18 @@ public static function download_entries($context, $course, $margic) {
$csv = new csv_export_writer();
$whichuser = ''; // Leave blank for an admin or teacher.
if (is_siteadmin($USER->id)) {
- $whichmargic = ('AND d.margic > 0');
+ $whichmargic = ('AND m.margic > 0');
$csv->filename = clean_filename(get_string('exportfilenameallentries', 'margic'));
} else if (has_capability('mod/margic:manageentries', $context)) {
- $whichmargic = ('AND d.margic = ');
+ $whichmargic = ('AND m.margic = ');
$whichmargic .= ($margic->id);
$csv->filename = clean_filename(get_string('exportfilenamemargicentries', 'margic'));
$csv->filename .= '_'.clean_filename(($course->shortname).'_');
$csv->filename .= clean_filename(($margic->name));
} else if (has_capability('mod/margic:addentries', $context)) {
- $whichmargic = ('AND d.margic = ');
+ $whichmargic = ('AND m.margic = ');
$whichmargic .= ($margic->id);
- $whichuser = (' AND d.userid = '.$USER->id); // Not an admin or teacher so can only get their OWN entries.
+ $whichuser = (' AND m.userid = '.$USER->id); // Not an admin or teacher so can only get their OWN entries.
$csv->filename = clean_filename(get_string('exportfilenamemyentries', 'margic'));
$csv->filename .= '_'.clean_filename(($course->shortname).'_');
$csv->filename .= clean_filename(($margic->name));
@@ -230,47 +230,47 @@ public static function download_entries($context, $course, $margic) {
// Add the headings to our data array.
$csv->add_data($fields);
if ($CFG->dbtype == 'pgsql') {
- $sql = "SELECT d.id AS entry,
+ $sql = "SELECT m.id AS entry,
u.firstname AS firstname,
u.lastname AS lastname,
- d.margic AS margic,
- d.userid AS userid,
- to_char(to_timestamp(d.timecreated), 'YYYY-MM-DD HH24:MI:SS') AS timecreated,
- to_char(to_timestamp(d.timemodified), 'YYYY-MM-DD HH24:MI:SS') AS timemodified,
- d.text AS text,
- d.format AS format,
- d.rating AS rating,
- d.feedback AS feedback,
- d.teacher AS teacher,
- to_char(to_timestamp(d.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked,
- d.baseentry AS baseentry
+ m.margic AS margic,
+ m.userid AS userid,
+ to_char(to_timestamp(m.timecreated), 'YYYY-MM-DD HH24:MI:SS') AS timecreated,
+ to_char(to_timestamp(m.timemodified), 'YYYY-MM-DD HH24:MI:SS') AS timemodified,
+ m.text AS text,
+ m.format AS format,
+ m.rating AS rating,
+ m.feedback AS feedback,
+ m.teacher AS teacher,
+ to_char(to_timestamp(m.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked,
+ m.baseentry AS baseentry
FROM {margic_entries} d
- JOIN {user} u ON u.id = d.userid
- WHERE d.userid > 0 ";
+ JOIN {user} u ON u.id = m.userid
+ WHERE m.userid > 0 ";
} else {
- $sql = "SELECT d.id AS entry,
+ $sql = "SELECT m.id AS entry,
u.firstname AS 'firstname',
u.lastname AS 'lastname',
- d.margic AS margic,
- d.userid AS userid,
- FROM_UNIXTIME(d.timecreated) AS TIMECREATED,
- FROM_UNIXTIME(d.timemodified) AS TIMEMODIFIED,
- d.text AS text,
- d.format AS format,
- d.rating AS rating,
- d.feedback AS feedback,
- d.teacher AS teacher,
- FROM_UNIXTIME(d.timemarked) AS TIMEMARKED,
- d.baseentry AS baseentry
+ m.margic AS margic,
+ m.userid AS userid,
+ FROM_UNIXTIME(m.timecreated) AS TIMECREATED,
+ FROM_UNIXTIME(m.timemodified) AS TIMEMODIFIED,
+ m.text AS text,
+ m.format AS format,
+ m.rating AS rating,
+ m.feedback AS feedback,
+ m.teacher AS teacher,
+ FROM_UNIXTIME(m.timemarked) AS TIMEMARKED,
+ m.baseentry AS baseentry
FROM {margic_entries} d
- JOIN {user} u ON u.id = d.userid
- WHERE d.userid > 0 ";
+ JOIN {user} u ON u.id = m.userid
+ WHERE m.userid > 0 ";
}
$sql .= ($whichmargic);
$sql .= ($whichuser);
- $sql .= " GROUP BY u.lastname, u.firstname, d.margic, d.id
- ORDER BY u.lastname ASC, u.firstname ASC, d.margic ASC, d.id ASC";
+ $sql .= " GROUP BY u.lastname, u.firstname, m.margic, m.id
+ ORDER BY u.lastname ASC, u.firstname ASC, m.margic ASC, m.id ASC";
// Add the list of users and diaries to our data array.
if ($ds = $DB->get_records_sql($sql, $fields)) {
diff --git a/classes/search/entry.php b/classes/search/entry.php
index 8c4c671..63a2c9a 100644
--- a/classes/search/entry.php
+++ b/classes/search/entry.php
@@ -53,17 +53,17 @@ class entry extends \core_search\base_mod {
public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
global $DB;
- list ($contextjoin, $contextparams) = $this->get_context_restriction_sql($context, 'margic', 'd', SQL_PARAMS_NAMED);
+ list ($contextjoin, $contextparams) = $this->get_context_restriction_sql($context, 'margic', 'm', SQL_PARAMS_NAMED);
if ($contextjoin === null) {
return null;
}
- $sql = "SELECT de.*, d.course
- FROM {margic_entries} de
- JOIN {margic} d ON d.id = de.margic
+ $sql = "SELECT me.*, m.course
+ FROM {margic_entries} me
+ JOIN {margic} m ON m.id = me.margic
$contextjoin
- WHERE de.timemodified >= :timemodified
- ORDER BY de.timemodified ASC";
+ WHERE me.timemodified >= :timemodified
+ ORDER BY me.timemodified ASC";
return $DB->get_recordset_sql($sql, array_merge($contextparams, [
'timemodified' => $modifiedfrom
]));
@@ -101,7 +101,7 @@ public function get_document($entry, $options = array()) {
$doc->set('userid', $entry->userid);
$doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
$doc->set('modified', $entry->timemodified);
- $doc->set('description1', content_to_text('Feedback: ' . $entry->feedback, $entry->format));
+ $doc->set('description1', content_to_text('Feedback: ' . $entry->feedback, $entry->formatfeedback));
// Check if this document should be considered new.
if (isset($options['lastindexedtime']) && ($options['lastindexedtime'] < $entry->timemodified)) {
diff --git a/db/log.php b/db/log.php
deleted file mode 100644
index 71ce052..0000000
--- a/db/log.php
+++ /dev/null
@@ -1,69 +0,0 @@
-.
-
-/**
- * Definition of log events
- *
- * @package mod_margic
- * @copyright 2022 coactum GmbH
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-defined('MOODLE_INTERNAL') || die();
-
-$logs = array(
- array(
- 'module' => 'margic',
- 'action' => 'view',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'view all',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'add entry',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'update entry',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'update feedback',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'download entries',
- 'mtable' => 'margic',
- 'field' => 'name'
- ),
- array(
- 'module' => 'margic',
- 'action' => 'invalid access attempt',
- 'mtable' => 'margic',
- 'field' => 'name'
- )
-);
diff --git a/edit.php b/edit.php
index a5fecb2..35a34b6 100644
--- a/edit.php
+++ b/edit.php
@@ -228,20 +228,17 @@
if ($entry && $fromform->entryid) {
// Trigger module entry updated event.
$event = \mod_margic\event\entry_updated::create(array(
- 'objectid' => $id,
+ 'objectid' => $newentry->id,
'context' => $context
));
} else {
// Trigger module entry created event.
$event = \mod_margic\event\entry_created::create(array(
- 'objectid' => $id,
+ 'objectid' => $newentry->id,
'context' => $context
));
}
- $event->add_record_snapshot('course_modules', $cm);
- $event->add_record_snapshot('course', $course);
- $event->add_record_snapshot('margic', $moduleinstance);
$event->trigger();
if ($moduleinstance->editentrydates && $fromform->timecreated > $timenow) {
diff --git a/error_summary.php b/error_summary.php
index 16f8c97..6d23464 100644
--- a/error_summary.php
+++ b/error_summary.php
@@ -120,7 +120,10 @@
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)];
+
+ if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) {
+ $typeswitched = $typeswitched[array_key_first($typeswitched)];
+ }
}
} else if ($type && $action == 2 &&
@@ -132,9 +135,12 @@
$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
+ if (!$typeswitched) { // If no type with priority+1 search for types with higher 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)];
+
+ if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) {
+ $typeswitched = $typeswitched[array_key_first($typeswitched)];
+ }
}
} else {
redirect($redirecturl, get_string('prioritynotchanged', 'mod_margic'), null, notification::NOTIFY_ERROR);
diff --git a/grade_entry.php b/grade_entry.php
index 3d90ade..804ea78 100644
--- a/grade_entry.php
+++ b/grade_entry.php
@@ -173,12 +173,9 @@
// Trigger module feedback updated event.
$event = \mod_margic\event\feedback_updated::create(array(
- 'objectid' => $id,
+ 'objectid' => $entry->id,
'context' => $context
));
- $event->add_record_snapshot('course_modules', $cm);
- $event->add_record_snapshot('course', $course);
- $event->add_record_snapshot('margic', $moduleinstance);
$event->trigger();
if ($fromform->sendgradingmessage) {
diff --git a/lang/de/margic.php b/lang/de/margic.php
index 813d5af..ac35fee 100644
--- a/lang/de/margic.php
+++ b/lang/de/margic.php
@@ -29,6 +29,9 @@
$string['eventdownloadentries'] = 'Margic Einträge herunterladen';
$string['evententrycreated'] = 'Margic Eintrag angelegt';
$string['evententryupdated'] = 'Margic Eintrag aktualisiert';
+$string['eventannotationcreated'] = 'Margic Annotation angelegt';
+$string['eventannotationupdated'] = 'Margic Annotation aktualisiert';
+$string['eventannotationdeleted'] = 'Margic Annotation gelöscht';
$string['eventfeedbackupdated'] = 'Feedback zu Margic Eintrag aktualisiert';
$string['eventinvalidaccess'] = 'Unberechtigter Zugriff';
@@ -143,7 +146,7 @@
$string['margicentrydate'] = 'Datum für diesen Eintrag festlegen';
$string['editentrynotpossible'] = 'Bearbeiten des Eintrags nicht möglich.';
$string['editdateinfuture'] = 'Das angegebene Erstelldatum des Eintrags liegt in der Zukunft.';
-$string['entryaddedoredited'] = 'Eintrag angelegt oder bearbeitet';
+$string['entryaddedoredited'] = 'Eintrag angelegt oder bearbeitet.';
$string['timecreatedinvalid'] = 'Änderung fehlgeschlagen. Es gibt bereits jüngere Versionen dieses Beitrags.';
$string['entryadded'] = 'Eintrag angelegt';
diff --git a/lang/en/margic.php b/lang/en/margic.php
index 3626d06..4aceb3b 100644
--- a/lang/en/margic.php
+++ b/lang/en/margic.php
@@ -28,6 +28,9 @@
$string['eventdownloadentries'] = 'Download entries';
$string['evententrycreated'] = 'Margic entry created';
$string['evententryupdated'] = 'Margic entry updated';
+$string['eventannotationcreated'] = 'Margic annotation created';
+$string['eventannotationupdated'] = 'Margic annotation updated';
+$string['eventannotationdeleted'] = 'Margic annotation deleted';
$string['eventfeedbackupdated'] = 'Margic feedback updated';
$string['eventinvalidaccess'] = 'Invalid access';
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache
index bf0c6b1..20aa59a 100644
--- a/templates/margic_entry.mustache
+++ b/templates/margic_entry.mustache
@@ -25,63 +25,6 @@
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": "",
- "annotations": [
- {
- "id": 1,
- ...
- },
- {
- "id": 2,
- ...
- }
- ],
}
}}
@@ -125,9 +68,9 @@
{{/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}}
+ {{#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}} {{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/timemodified}}
{{#annotationmode}}
diff --git a/version.php b/version.php
index 3f9ec3b..369af80 100644
--- a/version.php
+++ b/version.php
@@ -26,6 +26,6 @@
$plugin->component = 'mod_margic';
$plugin->release = '1.2.0'; // User-friendly version number.
-$plugin->version = 2022090200; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2022090400; // 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 afd654b..d58d43a 100644
--- a/view.php
+++ b/view.php
@@ -94,6 +94,13 @@
if (!empty($action) && $action == 'download' && has_capability('mod/margic:addentries', $context)) {
// Call download entries function in lib.php.
helper::download_entries($context, $course, $moduleinstance);
+
+ // Trigger module margic entries downloaded event.
+ $event = \mod_margic\event\download_margic_entries::create(array(
+ 'objectid' => $id,
+ 'context' => $context
+ ));
+ $event->trigger();
}
// Trigger course_module_viewed event.
From e71f3f9aa5d60953de2da82a102d4fbea7159162 Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Tue, 6 Sep 2022 18:53:08 +0200
Subject: [PATCH 56/60] fix(view): fix for downloading entries
---
classes/local/helper.php | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/classes/local/helper.php b/classes/local/helper.php
index 8c697e2..ba4996a 100644
--- a/classes/local/helper.php
+++ b/classes/local/helper.php
@@ -244,7 +244,7 @@ public static function download_entries($context, $course, $margic) {
m.teacher AS teacher,
to_char(to_timestamp(m.timemarked), 'YYYY-MM-DD HH24:MI:SS') AS timemarked,
m.baseentry AS baseentry
- FROM {margic_entries} d
+ FROM {margic_entries} m
JOIN {user} u ON u.id = m.userid
WHERE m.userid > 0 ";
} else {
@@ -262,7 +262,7 @@ public static function download_entries($context, $course, $margic) {
m.teacher AS teacher,
FROM_UNIXTIME(m.timemarked) AS TIMEMARKED,
m.baseentry AS baseentry
- FROM {margic_entries} d
+ FROM {margic_entries} m
JOIN {user} u ON u.id = m.userid
WHERE m.userid > 0 ";
}
@@ -273,27 +273,27 @@ public static function download_entries($context, $course, $margic) {
ORDER BY u.lastname ASC, u.firstname ASC, m.margic ASC, m.id ASC";
// Add the list of users and diaries to our data array.
- if ($ds = $DB->get_records_sql($sql, $fields)) {
- foreach ($ds as $d) {
- if ($d->timemodified == '1970-01-01 00:00:00') {
- $d->timemodified = '';
+ if ($ms = $DB->get_records_sql($sql, $fields)) {
+ foreach ($ms as $m) {
+ if ($m->timemodified == '1970-01-01 00:00:00') {
+ $m->timemodified = '';
}
$output = array(
- $d->entry,
- $d->firstname,
- $d->lastname,
- $d->margic,
- $d->userid,
- $d->timecreated,
- $d->timemodified,
- $d->format,
- $d->rating,
- $d->feedback,
- $d->teacher,
- $d->timemarked,
- $d->baseentry,
- format_text($d->text, $d->format, array('para' => false))
+ $m->entry,
+ $m->firstname,
+ $m->lastname,
+ $m->margic,
+ $m->userid,
+ $m->timecreated,
+ $m->timemodified,
+ $m->format,
+ $m->rating,
+ $m->feedback,
+ $m->teacher,
+ $m->timemarked,
+ $m->baseentry,
+ format_text($m->text, $m->format, array('para' => false))
);
$csv->add_data($output);
}
From fe65fe70222f7847e5c4052e567fd8f6d5db7234 Mon Sep 17 00:00:00 2001
From: Daniel Nolte <>
Date: Wed, 7 Sep 2022 17:14:11 +0200
Subject: [PATCH 57/60] fix(security): added sesskey() check for multiple
actions (and done some chore)
---
amd/build/annotations.min.js | 2 +-
amd/build/annotations.min.js.map | 2 +-
amd/src/annotations.js | 3 --
annotations.php | 12 ++++----
backup/moodle2/restore_margic_stepslib.php | 2 +-
classes/local/helper.php | 1 -
classes/output/margic_entry.php | 7 ++++-
classes/output/margic_error_summary.php | 7 ++++-
classes/output/margic_view.php | 2 +-
classes/privacy/provider.php | 32 ++--------------------
db/access.php | 4 +--
edit.php | 4 +--
error_summary.php | 16 +++++++----
grade_entry.php | 12 ++++----
lib.php | 18 ++----------
locallib.php | 6 ++++
settings.php | 2 +-
templates/margic_childentry.mustache | 4 +--
templates/margic_entry.mustache | 4 +--
templates/margic_error_summary.mustache | 10 +++----
templates/margic_view.mustache | 10 +++----
view.php | 2 ++
22 files changed, 73 insertions(+), 89 deletions(-)
diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js
index d43c6ea..96d5bdc 100644
--- a/amd/build/annotations.min.js
+++ b/amd/build/annotations.min.js
@@ -5,6 +5,6 @@ define("mod_margic/annotations",["exports","jquery","./highlighting"],(function(
* @module mod_margic/annotations
* @copyright 2022 coactum GmbH
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;var annotation={target:ranges.map((function(range){return(0,_highlighting.describe)(root,range)})).map((function(selectors){return{selector:selectors}}))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0]),(0,_jquery.default)("#annotationpreview-"+annotation.id).html(annotation.exact.replaceAll("<","<").replaceAll(">",">"))}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching annotations")}})}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=function(cmid,canmakeannotations,myuserid){var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&myuserid==annotations[annotationid].userid){(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=annotationid;var entry=annotations[annotationid].entry;(0,_jquery.default)(".annotation-box-"+annotationid).hide(),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(annotations[annotationid].startcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(annotations[annotationid].endcontainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(annotations[annotationid].startoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(annotations[annotationid].endoffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(annotations[annotationid].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(annotations[annotationid].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(annotations[annotationid].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(annotations[annotationid].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(annotations[annotationid].suffix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationid"]').val(annotationid),(0,_jquery.default)(".annotation-form-"+entry+' textarea[name="text"]').val(annotations[annotationid].text),(0,_jquery.default)(".annotation-form-"+entry+" select").val(annotations[annotationid].type),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(annotations[annotationid].exact.replaceAll("<","<").replaceAll(">",">")),(0,_jquery.default)("#annotationpreview-temp-"+entry).css("border-color","#"+annotations[annotationid].color),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").insertBefore(".annotation-box-"+annotationid),(0,_jquery.default)(".annotationarea-"+entry+" .annotation-form").show(),(0,_jquery.default)(".annotationarea-"+entry+" #id_text").focus()}else(0,_jquery.default)(".annotation-box-"+annotationid).focus()}function resetForms(){(0,_jquery.default)(".annotation-form").hide(),(0,_jquery.default)('.annotation-form input[name^="annotationid"]').val(null),(0,_jquery.default)('.annotation-form input[name^="startcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endcontainer"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="startoffset"]').val(-1),(0,_jquery.default)('.annotation-form input[name^="endoffset"]').val(-1),(0,_jquery.default)('.annotation-form textarea[name^="text"]').val(""),(0,_jquery.default)(".annotation-box").not(".annotation-form").show()}(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(),(0,_highlighting.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(){if(""!==window.getSelection().getRangeAt(0).cloneContents().textContent&&canmakeannotations){(0,_highlighting.removeAllTempHighlights)(),resetForms(),newannotation=function(root){var ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;var annotation={target:ranges.map((function(range){return(0,_highlighting.describe)(root,range)})).map((function(selectors){return{selector:selectors}}))};return(0,_highlighting.anchor)(annotation,root),annotation}(this);var entry=this.id.replace(/entry-/,"");(0,_jquery.default)(".annotation-form-"+entry+' input[name="startcontainer"]').val(newannotation.target[0].selector[0].startContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endcontainer"]').val(newannotation.target[0].selector[0].endContainer),(0,_jquery.default)(".annotation-form-"+entry+' input[name="startoffset"]').val(newannotation.target[0].selector[0].startOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="endoffset"]').val(newannotation.target[0].selector[0].endOffset),(0,_jquery.default)(".annotation-form-"+entry+' input[name="start"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="end"]').val(newannotation.target[0].selector[1].end),(0,_jquery.default)(".annotation-form-"+entry+' input[name="exact"]').val(newannotation.target[0].selector[2].exact),(0,_jquery.default)(".annotation-form-"+entry+' input[name="prefix"]').val(newannotation.target[0].selector[2].prefix),(0,_jquery.default)(".annotation-form-"+entry+' input[name="suffix"]').val(newannotation.target[0].selector[2].suffix),(0,_jquery.default)(".annotation-form-"+entry+" select").val(1),(0,_jquery.default)("#annotationpreview-temp-"+entry).html(newannotation.target[0].selector[2].exact.replaceAll("<","<").replaceAll(">",">")),(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],_newannotation={annotation:annotation,target:[[{type:"RangeSelector",startContainer:annotation.startcontainer,startOffset:parseInt(annotation.startoffset),endContainer:annotation.endcontainer,endOffset:parseInt(annotation.endoffset)},{type:"TextPositionSelector",start:parseInt(annotation.start),end:parseInt(annotation.end)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((function(selectors){return{selector:selectors}}))};(0,_highlighting.anchor)(_newannotation,(0,_jquery.default)("#entry-"+annotation.entry)[0])}}(),(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")}))},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){alert("Error fetching 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 d0e3284..4d2a853 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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\n\n // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-' + annotation.id).html(annotation.exact.replaceAll('<', '<').replaceAll('>', '>'));\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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 // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","annotation","target","map","range","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","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';\nimport {removeAllTempHighlights, anchor, describe} from './highlighting';\n\nexport const init = (cmid, canmakeannotations, myuserid) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\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(); // Reset the annotation forms.\n\n // Create new annotation.\n newannotation = createAnnotation(this);\n\n var entry = this.id.replace(/entry-/, '');\n\n // RangeSelector.\n $('.annotation-form-' + entry + ' input[name=\"startcontainer\"]').val(\n newannotation.target[0].selector[0].startContainer);\n $('.annotation-form-' + entry + ' input[name=\"endcontainer\"]').val(\n newannotation.target[0].selector[0].endContainer);\n $('.annotation-form-' + entry + ' input[name=\"startoffset\"]').val(\n newannotation.target[0].selector[0].startOffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(\n newannotation.target[0].selector[0].endOffset);\n\n // TextPositionSelector.\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(\n newannotation.target[0].selector[1].end);\n\n // TextQuoteSelector.\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(\n newannotation.target[0].selector[2].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(\n newannotation.target[0].selector[2].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(\n newannotation.target[0].selector[2].suffix);\n\n $('.annotation-form-' + entry + ' select').val(1);\n\n // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n newannotation.target[0].selector[2].exact.replaceAll('<', '<').replaceAll('>', '>'));\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 complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n alert('Error fetching annotations');\n }\n });\n\n /**\n * Recreate annotations.\n *\n */\n function recreateAnnotations() {\n\n for (let annotation of Object.values(annotations)) {\n\n const rangeSelectors = [[\n {type: \"RangeSelector\", startContainer: annotation.startcontainer, startOffset: parseInt(annotation.startoffset),\n endContainer: annotation.endcontainer, endOffset: parseInt(annotation.endoffset)},\n {type: \"TextPositionSelector\", start: parseInt(annotation.start), end: parseInt(annotation.end)},\n {type: \"TextQuoteSelector\", exact: annotation.exact, prefix: annotation.prefix, suffix: annotation.suffix}\n ]];\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const newannotation = {\n annotation: annotation,\n target: target,\n };\n\n anchor(newannotation, $(\"#entry-\" + annotation.entry)[0]);\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=\"startoffset\"]').val(annotations[annotationid].startoffset);\n $('.annotation-form-' + entry + ' input[name=\"endoffset\"]').val(annotations[annotationid].endoffset);\n $('.annotation-form-' + entry + ' input[name=\"start\"]').val(annotations[annotationid].start);\n $('.annotation-form-' + entry + ' input[name=\"end\"]').val(annotations[annotationid].end);\n $('.annotation-form-' + entry + ' input[name=\"exact\"]').val(annotations[annotationid].exact);\n $('.annotation-form-' + entry + ' input[name=\"prefix\"]').val(annotations[annotationid].prefix);\n $('.annotation-form-' + entry + ' input[name=\"suffix\"]').val(annotations[annotationid].suffix);\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 // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).\n $('#annotationpreview-temp-' + entry).html(\n annotations[annotationid].exact.replaceAll('<', '<').replaceAll('>', '>'));\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^=\"startoffset\"]').val(-1);\n $('.annotation-form input[name^=\"endoffset\"]').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/**\n * Create a new annotation that is associated with the selected region of\n * the current document.\n *\n * @param {object} root - The root element\n * @return {object} - The new annotation\n */\nfunction createAnnotation(root) {\n const ranges = [window.getSelection().getRangeAt(0)];\n\n if (ranges.collapsed) {\n return null;\n }\n\n const rangeSelectors = ranges.map(range => describe(root, range));\n\n const target = rangeSelectors.map(selectors => ({\n selector: selectors,\n }));\n\n /** @type {AnnotationData} */\n const annotation = {\n target,\n };\n\n anchor(annotation, root);\n\n return annotation;\n}"],"names":["cmid","canmakeannotations","myuserid","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","start","end","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","annotation","target","map","range","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","complete","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 883a0c2..15c9e60 100644
--- a/amd/src/annotations.js
+++ b/amd/src/annotations.js
@@ -194,9 +194,6 @@ export const init = (cmid, canmakeannotations, myuserid) => {
};
anchor(newannotation, $("#entry-" + annotation.entry)[0]);
-
- // Prevent JavaScript injection (if annotated text in original entry is JavaScript code in script tags).
- $('#annotationpreview-' + annotation.id).html(annotation.exact.replaceAll('<', '<').replaceAll('>', '>'));
}
}
diff --git a/annotations.php b/annotations.php
index 3094a12..751dbb5 100644
--- a/annotations.php
+++ b/annotations.php
@@ -92,6 +92,8 @@
// Delete annotation.
if (has_capability('mod/margic:makeannotations', $context) && $deleteannotation !== 0) {
+ require_sesskey();
+
global $USER;
if ($DB->record_exists('margic_annotations', array('id' => $deleteannotation, 'margic' => $moduleinstance->id, 'userid' => $USER->id))) {
@@ -111,7 +113,7 @@
}
}
-// Save annotation
+// Save annotation.
require_once($CFG->dirroot . '/mod/margic/annotation_form.php');
// Instantiate form.
@@ -179,10 +181,10 @@
$annotation->endoffset = $fromform->endoffset;
$annotation->start = $fromform->start;
$annotation->end = $fromform->end;
- $annotation->exact = format_text($fromform->exact, 2, array('para' => false));;
- $annotation->prefix = format_text($fromform->prefix, 2, array('para' => false));;
- $annotation->suffix = format_text($fromform->suffix, 2, array('para' => false));;
- $annotation->text = format_text($fromform->text, 2, array('para' => false));
+ $annotation->exact = $fromform->exact;
+ $annotation->prefix = $fromform->prefix;
+ $annotation->suffix = $fromform->suffix;
+ $annotation->text = $fromform->text;
$newid = $DB->insert_record('margic_annotations', $annotation);
diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php
index 91057f5..eeeae69 100644
--- a/backup/moodle2/restore_margic_stepslib.php
+++ b/backup/moodle2/restore_margic_stepslib.php
@@ -116,7 +116,7 @@ protected function process_margic_entry($data) {
}
$newitemid = $DB->insert_record('margic_entries', $data);
- $this->set_mapping('margic_entry', $oldid, $newitemid, true); // The true parameter is necessary for file handling
+ $this->set_mapping('margic_entry', $oldid, $newitemid, true); // The true parameter is necessary for file handling.
}
/**
diff --git a/classes/local/helper.php b/classes/local/helper.php
index ba4996a..e589166 100644
--- a/classes/local/helper.php
+++ b/classes/local/helper.php
@@ -501,7 +501,6 @@ 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->{'rating_' . $entry->id} = $entry->rating;
diff --git a/classes/output/margic_entry.php b/classes/output/margic_entry.php
index 44537f4..8d4cddf 100644
--- a/classes/output/margic_entry.php
+++ b/classes/output/margic_entry.php
@@ -95,6 +95,8 @@ class margic_entry implements renderable, templatable {
protected $gradingstr;
/** @var string */
protected $regradingstr;
+ /** @var string */
+ protected $sesskey;
/**
* Construct this renderable.
* @param object $margic The margic obj
@@ -121,11 +123,12 @@ class margic_entry implements renderable, templatable {
* @param object $strmanager The strmanager
* @param string $gradingstr The gradingstr
* @param string $regradingstr The regradingstr
+ * @param string $sesskey The session key
*/
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) {
+ $strmanager, $gradingstr, $regradingstr, $sesskey) {
$this->margic = $margic;
$this->cm = $cm;
@@ -153,6 +156,7 @@ public function __construct($margic, $cm, $context, $moduleinstance, $entry, $an
$this->strmanager = $strmanager;
$this->gradingstr = $gradingstr;
$this->regradingstr = $regradingstr;
+ $this->sesskey = $sesskey;
}
/**
@@ -183,6 +187,7 @@ public function export_for_template(renderer_base $output) {
$data->textbgc = get_config('margic', 'textbgc');
$data->errortypes = $this->errortypes;
$data->readonly = $this->readonly;
+ $data->sesskey = $this->sesskey;
return $data;
}
}
diff --git a/classes/output/margic_error_summary.php b/classes/output/margic_error_summary.php
index 68ff8e6..46d22b4 100644
--- a/classes/output/margic_error_summary.php
+++ b/classes/output/margic_error_summary.php
@@ -45,19 +45,23 @@ class margic_error_summary implements renderable, templatable {
protected $margicerrortypes;
/** @var object */
protected $errortypetemplates;
+ /** @var string */
+ protected $sesskey;
/**
* Construct this renderable.
* @param int $cmid The course module id
* @param array $participants The participants 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
+ * @param string $sesskey The session key
*/
- public function __construct($cmid, $participants, $margicerrortypes, $errortypetemplates) {
+ public function __construct($cmid, $participants, $margicerrortypes, $errortypetemplates, $sesskey) {
$this->cmid = $cmid;
$this->participants = $participants;
$this->margicerrortypes = $margicerrortypes;
$this->errortypetemplates = $errortypetemplates;
+ $this->sesskey = $sesskey;
}
/**
@@ -72,6 +76,7 @@ public function export_for_template(renderer_base $output) {
$data->participants = $this->participants;
$data->margicerrortypes = $this->margicerrortypes;
$data->errortypetemplates = $this->errortypetemplates;
+ $data->sesskey = $this->sesskey;
return $data;
}
diff --git a/classes/output/margic_view.php b/classes/output/margic_view.php
index 399be86..e912ac9 100644
--- a/classes/output/margic_view.php
+++ b/classes/output/margic_view.php
@@ -195,7 +195,7 @@ public function export_for_template(renderer_base $output) {
$this->entries[$key]->entry = $OUTPUT->render(new margic_entry($this->margic, $this->cm, $this->context, $this->moduleinstance,
$entry, $this->annotationareawidth, $this->moduleinstance->editentries, $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));
+ $this->canmakeannotations, $this->errortypes, $readonly, $grades, $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr, $this->sesskey));
}
}
}
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index ed13e44..f660d65 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -35,8 +35,6 @@
use core_privacy\local\request\user_preference_provider;
-use core_grades\component_gradeitem as gradeitem; // needed?
-
/**
* Privacy class for requesting user data.
*
@@ -496,13 +494,7 @@ public static function delete_data_for_all_users_in_context(\context $context) {
return;
}
- // Delete advanced grading information.
- /* $gradingmanager = get_grading_manager($context, 'mod_margic', 'margic');
- $controller = $gradingmanager->get_active_controller();
-
- if (isset($controller)) {
- \core_grading\privacy\provider::delete_instance_data($context);
- } */
+ // Delete advanced grading information (not implemented yet).
// Delete all ratings in the context.
\core_rating\privacy\provider::delete_ratings($context, 'mod_margic', 'entry');
@@ -537,16 +529,7 @@ public static function delete_data_for_user(approved_contextlist $contextlist) {
// Get the course module.
$cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
- // Handle any advanced grading method data first.
- /* $grades = $DB->get_records('margic_entries', ['margic' => $cm->instance, 'userid' => $userid]);
- $gradingmanager = get_grading_manager($context, 'margic_entries', 'margic');
- $controller = $gradingmanager->get_active_controller();
- foreach ($grades as $grade) {
- // Delete advanced grading information.
- if (isset($controller)) {
- \core_grading\privacy\provider::delete_instance_data($context, $grade->id);
- }
- } */
+ // Handle any advanced grading method data first (not implemented yet).
// Delete ratings.
$entriessql = "SELECT
@@ -613,16 +596,7 @@ public static function delete_data_for_users(approved_userlist $userlist) {
list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
$params = array_merge(['margicid' => $cm->instance], $userinparams);
- // Handle any advanced grading method data first.
- /* $grades = $DB->get_records('margic_entries', ['margic' => $cm->instance, 'userid' => $userid]);
- $gradingmanager = get_grading_manager($context, 'margic_entries', 'margic');
- $controller = $gradingmanager->get_active_controller();
- foreach ($grades as $grade) {
- // Delete advanced grading information.
- if (isset($controller)) {
- \core_grading\privacy\provider::delete_instance_data($context, $grade->id);
- }
- } */
+ // Handle any advanced grading method data first (not implemented yet).
// Delete ratings.
$entriesselect = "SELECT
diff --git a/db/access.php b/db/access.php
index 7190d43..114101d 100644
--- a/db/access.php
+++ b/db/access.php
@@ -71,7 +71,7 @@
),
'mod/margic:makeannotations' => array(
- 'riskbitmask' => RISK_XSS | RISK_SPAM,
+ 'riskbitmask' => RISK_XSS | RISK_SPAM | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
@@ -94,7 +94,7 @@
),
'mod/margic:editdefaulterrortypes' => array(
- 'riskbitmask' => RISK_XSS,
+ 'riskbitmask' => RISK_XSS | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => array(
diff --git a/edit.php b/edit.php
index 35a34b6..3b6340f 100644
--- a/edit.php
+++ b/edit.php
@@ -187,7 +187,7 @@
$newentry->baseentry = $entry->baseentry;
}
- // Check if timecreated is not older then connected entries
+ // Check if timecreated is not older then connected entries.
if ($moduleinstance->editentrydates) {
$baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry));
@@ -285,7 +285,7 @@
$page = new margic_entry($margic, $cm, $context, $moduleinstance, $entry, $margic->get_annotationarea_width(),
$moduleinstance->editentries, $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);
+ false, true, $grades, $currentgroups, $allowedusers, $strmanager, $gradingstr, $regradingstr, sesskey());
echo $OUTPUT->render($page);
}
diff --git a/error_summary.php b/error_summary.php
index 6d23464..9430805 100644
--- a/error_summary.php
+++ b/error_summary.php
@@ -79,6 +79,8 @@
// Add type to margic.
if ($addtomargic) {
+ require_sesskey();
+
$redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id));
if ($DB->record_exists('margic_errortype_templates', array('id' => $addtomargic))) {
@@ -104,6 +106,8 @@
// Change priority.
if ($mode == 2 && $priority && $action && $DB->record_exists('margic_errortypes', array('id' => $priority))) {
+ require_sesskey();
+
$redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id));
$type = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'id' => $priority));
@@ -111,14 +115,14 @@
$prioritychanged = false;
$oldpriority = 0;
- if ($type && $action == 1 && $type->priority != 1) { // Increase priority (show more in front)
+ 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
+ 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');
if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) {
@@ -127,7 +131,7 @@
}
} else if ($type && $action == 2 &&
- $type->priority != $DB->count_records('margic_errortypes', array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back)
+ $type->priority != $DB->count_records('margic_errortypes', array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back).
$oldpriority = $type->priority;
$type->priority += 1;
@@ -135,7 +139,7 @@
$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 higher priority values
+ if (!$typeswitched) { // If no type with priority+1 search for types with higher priority values.
$typeswitched = $DB->get_records_select('margic_errortypes', "margic = $moduleinstance->id AND priority > $type->priority", null, 'priority ASC');
if ($typeswitched && isset($typeswitched[array_key_first($typeswitched)])) {
@@ -163,6 +167,8 @@
// Delete annotation.
if ($delete !== 0 && $mode) {
+ require_sesskey();
+
$redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id));
if ($mode == 1) { // If type is template error type.
@@ -280,7 +286,7 @@
$errortypetemplates = array_values($errortypetemplates);
// Output page.
-$page = new margic_error_summary($cm->id, $participants, $margicerrortypes, $errortypetemplates);
+$page = new margic_error_summary($cm->id, $participants, $margicerrortypes, $errortypetemplates, sesskey());
echo $OUTPUT->render($page);
diff --git a/grade_entry.php b/grade_entry.php
index 804ea78..b653149 100644
--- a/grade_entry.php
+++ b/grade_entry.php
@@ -193,26 +193,26 @@
$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->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 = '
';
$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->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
+ . ' - ' . 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
+ $content = array('*' => array('header' => $header, 'footer' => $footer)); // Extra content for specific processor.
$message->set_additional_content('email', $content);
- // Actually send the message
+ // Actually send the message.
$messageid = message_send($message);
}
diff --git a/lib.php b/lib.php
index 45d1ac0..f2e6494 100644
--- a/lib.php
+++ b/lib.php
@@ -105,7 +105,7 @@ function margic_update_instance($margic) {
// If the aggregation type or scale (i.e. max grade) changes then recalculate the grades for the entire margic
// if scale changes - do we need to recheck the ratings, if ratings higher than scale how do we want to respond?
- // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade
+ // for count and sum aggregation types the grade we check to make sure they do not exceed the scale (i.e. max score) when calculating the grade.
$oldmargic = $DB->get_record('margic', array('id' => $margic->id));
$updategrades = false;
@@ -468,7 +468,7 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour
continue;
}
- if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
+ if ($groupmode == SEPARATEGROUPS && !$accessallgroups) {
if (isguestuser()) {
// Shortcut - guest user does not belong into any group.
continue;
@@ -562,18 +562,6 @@ function margic_print_recent_mod_activity($activity, $courseid, $detail, $modnam
echo '';
}
- /* if (isset($activity->grade)) {
- echo '
{{/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":"i2DAaSA,eAAeC,aACdA,KAAKC,eACNC,KAAKC,kBACLD,KAAKE,iBAIsBJ,KAAKK,YAAaC,sBAEzC,YASJC,2BAA2BP,cAC9BQ,QAAUR,KAAKS,gBACfH,OAAS,EAENE,SACLF,QAAUP,eAAeS,SACzBA,QAAUA,QAAQC,uBAGbH,gBAWAI,eAAeC,uCAAYC,2DAAAA,wCAS9BC,SAPAC,WAAaF,QAAQG,QACnBC,SACJL,QAAQM,cACRC,mBAAmBP,QAASQ,WAAWC,WACnCC,QAAU,GAEZC,YAAcN,SAASO,WAEvBjB,OAAS,OAISkB,IAAfV,YAA4BQ,aAG7BhB,QAFJO,SAAgCS,aAEVG,KAAKnB,OAASQ,YAClCO,QAAQK,KAAK,CAAE1B,KAAMa,SAAUc,OAAQb,WAAaR,SACpDQ,WAAaF,QAAQG,UAErBO,YAAcN,SAASO,WACvBjB,QAAUO,SAASY,KAAKnB,kBAKNkB,IAAfV,YAA4BD,UAAYP,SAAWQ,YACxDO,QAAQK,KAAK,CAAE1B,KAAMa,SAAUc,OAAQd,SAASY,KAAKnB,SACrDQ,WAAaF,QAAQG,gBAGJS,IAAfV,iBACI,IAAIc,WAAW,qCAGhBP,+LAGqB,6BACC,MAQlBQ,8CAQClB,QAASgB,8CACfA,OAAS,QACL,IAAIG,MAAM,0BAIbnB,QAAUA,aAGVgB,OAASA,iEAUhB,SAAWI,YACJA,OAAOC,SAASC,KAAKtB,eAClB,IAAImB,MAAM,wDAGdI,GAAKD,KAAKtB,QACVgB,OAASM,KAAKN,OACXO,KAAOH,QACZJ,QAAUpB,2BAA2B2B,IACrCA,GAA6BA,GAAGC,qBAG3B,IAAIN,aAAaK,GAAIP,+BAqB9B,eAAQS,+DAAU,cAEP1B,eAAeuB,KAAKtB,QAASsB,KAAKN,QAAQ,GACjD,MAAOU,QACa,IAAhBJ,KAAKN,aAAsCH,IAAtBY,QAAQE,UAAyB,KAClDC,GAAKC,SAASC,iBAClBR,KAAKtB,QAAQ+B,cACbvB,WAAWC,WAEbmB,GAAGjB,YAAcW,KAAKtB,YAChBgC,SA/EgB,IA+ELP,QAAQE,UACnBM,KACJD,SAAWJ,GAAGhB,WAAagB,GAAGM,mBAE3BD,WACGP,UAED,CAAErC,KAAM4C,KAAMjB,OAAQgB,SAAW,EAAIC,KAAKnB,KAAKnB,cAEhD+B,qCAaZ,SAAsBrC,KAAM2B,eAClB3B,KAAKC,eACNC,KAAKE,iBACDyB,aAAaiB,UAAU9C,KAAM2B,aACjCzB,KAAKC,oBACD,IAAI0B,aAAqC7B,KAAO2B,sBAEjD,IAAIG,MAAM,iEAWtB,SAAiB9B,KAAM2B,eAEb3B,KAAKC,eACNC,KAAKE,aACJuB,OAAS,GAAKA,OAA8B3B,KAAMyB,KAAKnB,aACnD,IAAIwB,MAAM,wCAGb9B,KAAKmC,oBACF,IAAIL,MAAM,+BAIZiB,WAAaxC,2BAA2BP,MAAQ2B,cAE/C,IAAIE,aAAa7B,KAAKmC,cAAeY,iBAEzC7C,KAAKC,gBACJwB,OAAS,GAAKA,OAAS3B,KAAKgD,WAAW1C,aACnC,IAAIwB,MAAM,6CAIdiB,YAAa,EACRE,EAAI,EAAGA,EAAItB,OAAQsB,IAC1BF,aAAchD,eAAeC,KAAKgD,WAAWC,WAGxC,IAAIpB,aAAqC7B,KAAO+C,2BAGjD,IAAIjB,MAAM,uGAYXoB,wCAOCC,MAAOC,0CACZD,MAAQA,WACRC,IAAMA,2DAUb,SAAWzC,gBACF,IAAIuC,UACTjB,KAAKkB,MAAME,WAAW1C,SACtBsB,KAAKmB,IAAIC,WAAW1C,iCAexB,eACMwC,MACAC,OAGFnB,KAAKkB,MAAMxC,UAAYsB,KAAKmB,IAAIzC,SAChCsB,KAAKkB,MAAMxB,QAAUM,KAAKmB,IAAIzB,OAC9B,qCAEejB,eACbuB,KAAKkB,MAAMxC,QACXsB,KAAKkB,MAAMxB,OACXM,KAAKmB,IAAIzB,WAHVwB,0BAAOC,6BAMRD,MAAQlB,KAAKkB,MAAMG,QAAQ,CAAChB,UAtNJ,IAuNxBc,IAAMnB,KAAKmB,IAAIE,QAAQ,CAAChB,UAtNC,QAyNrBiB,MAAQ,IAAIC,aAClBD,MAAME,SAASN,MAAMnD,KAAMmD,MAAMxB,QACjC4B,MAAMG,OAAON,IAAIpD,KAAMoD,IAAIzB,QACpB4B,iCAST,SAAiBA,cAMR,IAAIL,UALGrB,aAAaiB,UACzBS,MAAMI,eACNJ,MAAMK,aAEI/B,aAAaiB,UAAUS,MAAMM,aAAcN,MAAMO,uCAY/D,SAAmBC,KAAMZ,MAAOC,YACvB,IAAIF,UACT,IAAIrB,aAAakC,KAAMZ,OACvB,IAAItB,aAAakC,KAAMX"}
\ 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):
- {
- }
}}