diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7aeb7db..2d6119e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,8 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.3', '7.4', '8.0', '8.1'] - moodle-branch: ['MOODLE_39_STABLE', 'MOODLE_310_STABLE', 'MOODLE_311_STABLE', 'MOODLE_400_STABLE', 'MOODLE_401_STABLE', 'MOODLE_402_STABLE'] + php: ['7.3', '7.4', '8.0', '8.1', '8.2'] + moodle-branch: ['MOODLE_39_STABLE', 'MOODLE_310_STABLE', 'MOODLE_311_STABLE', 'MOODLE_400_STABLE', 'MOODLE_401_STABLE', 'MOODLE_402_STABLE', 'MOODLE_403_STABLE'] database: [pgsql, mariadb] steps: diff --git a/CHANGES.md b/CHANGES.md index 361c5f9..abb141a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,22 @@ ## Changelog ## + +- [1.3.1]: + - Ensured compatibility with Moodle 4.3. + - Changed code to comply with new moodle coding standards. + - [Bugfix]: Changed default error type template colors for new installations to improve contrast. + +- [1.3.0]: + - [Bugfix]: Fixed a bug that prevented the order of Margic error types (that were subsequently added to an instance from an error type template) from being changed under certain conditions. + - [Bugfix]: Deleting error types now triggers a confirm prompt. + - [Bugfix]: Removed doubled triggering of the download_margic_entries event. + - [Feature]: Added a color picker for creating error types and templates. + - [Improvement]: The default value for the feedback notification for the entries can now be set. Administrators can set the default value in the admin settings. This is then taken as the default value when a new margic is created, but can be changed there by teachers for the entire margic. Of course, teachers can deviate from the default value for each grading in the actual grading form. If the admin does not change the default value, it remains true as it was until now. + - [Improvement]: You can now prevent the displaying of timestamps for entries, annotations and feedback. There are now three new capabilities: "viewotherusersentrytimes" determines whether a user sees when an entry made by other users was created. "viewotherusersannotationtimes" determines whether a user can see when annotations were created by other users. "viewotherusersfeedbacktimes" determines whether a user can see when other teachers have given feedback on an entry. All three capabilities are activated for all users by default, but you can now withdraw these permission for individual roles (e.g. if you do not want the participants to see the times at which the teachers create their annotations or when they give feedback). + - [Improvement]: You can now define for each Margic if teachers can overwrite and delete the annotations made by other teachers. + - [Improvement]: If you save feedback or grading the page now jumps to the changed feedback after if is saved. + - [Improvement]: Annotation button now in a different color when annotation mode is activated. + - [Change]: Removed the link to index.php in the course navigation (use the course block instead). + - [1.2.9]: - Ensured compatibility with Moodle 4.2. - [Layout]: Minor layout fixes because of the new versions of the bootstrap and fontawesome libraries. diff --git a/amd/build/annotations.min.js b/amd/build/annotations.min.js index 245842b..947af56 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=(cmid,canmakeannotations,myuserid,focusannotation)=>{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="annotationstart"]').val(annotations[annotationid].annotationstart),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationend"]').val(annotations[annotationid].annotationend),(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",".margic_entry #id_cancel",(function(e){e.preventDefault(),(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1})),(0,_jquery.default)(".margic_entry 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){const ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;const rangeSelectors=ranges.map((range=>(0,_highlighting.describe)(root,range))),annotation={target:rangeSelectors.map((selectors=>({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="annotationstart"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationend"]').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(let annotation of Object.values(annotations)){const 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.annotationstart),end:parseInt(annotation.annotationend)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((selectors=>({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).css("background-color","lightblue")})),(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).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),(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).css("background-color","lightblue")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),0!=focusannotation&&((0,_jquery.default)(".annotated-"+focusannotation).attr("tabindex",-1),(0,_jquery.default)(".annotated-"+focusannotation).focus())},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){}})}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=(cmid,canmakeannotations,myuserid,focusannotation,focusgradingform,overwriteannotations)=>{var edited=!1,annotations=Array(),newannotation=!1;function editAnnotation(annotationid){if(edited==annotationid)(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1;else if(canmakeannotations&&(overwriteannotations||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="annotationstart"]').val(annotations[annotationid].annotationstart),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationend"]').val(annotations[annotationid].annotationend),(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()}focusgradingform&&((0,_jquery.default)(".gradingform").addClass("show"),(0,_jquery.default)("#id_feedback_"+focusgradingform+"_editoreditable").focus()),(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",".margic_entry #id_cancel",(function(e){e.preventDefault(),(0,_highlighting.removeAllTempHighlights)(),resetForms(),edited=!1})),(0,_jquery.default)(".margic_entry 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){const ranges=[window.getSelection().getRangeAt(0)];if(ranges.collapsed)return null;const rangeSelectors=ranges.map((range=>(0,_highlighting.describe)(root,range))),annotation={target:rangeSelectors.map((selectors=>({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="annotationstart"]').val(newannotation.target[0].selector[1].start),(0,_jquery.default)(".annotation-form-"+entry+' input[name="annotationend"]').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(let annotation of Object.values(annotations)){const 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.annotationstart),end:parseInt(annotation.annotationend)},{type:"TextQuoteSelector",exact:annotation.exact,prefix:annotation.prefix,suffix:annotation.suffix}]].map((selectors=>({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).css("background-color","lightblue")})),(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).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),(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).css("background-color","lightblue")})),(0,_jquery.default)(document).on("mouseleave",".hoverannotation",(function(){var id=this.id.replace("hoverannotation-","");(0,_jquery.default)(".annotated-"+id).css("background-color",(0,_jquery.default)(".annotated-"+id).css("textDecorationColor"))})),0!=focusannotation&&((0,_jquery.default)(".annotated-"+focusannotation).attr("tabindex",-1),(0,_jquery.default)(".annotated-"+focusannotation).focus())},complete:function(){(0,_jquery.default)("#overlay").hide()},error:function(){}})}})); //# 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 48f7162..4036d24 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, focusannotation) => {\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', '.margic_entry #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 $('.margic_entry 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=\"annotationstart\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"annotationend\"]').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).css(\"background-color\", 'lightblue');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\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).css(\"background-color\", 'lightblue');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n\n // Focus annotation if needed.\n if (focusannotation != 0) {\n $('.annotated-' + focusannotation).attr('tabindex', -1);\n $('.annotated-' + focusannotation).focus();\n }\n\n },\n complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n // For output: 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.annotationstart),\n end: parseInt(annotation.annotationend)},\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=\"annotationstart\"]').val(annotations[annotationid].annotationstart);\n $('.annotation-form-' + entry + ' input[name=\"annotationend\"]').val(annotations[annotationid].annotationend);\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","focusannotation","edited","annotations","Array","newannotation","editAnnotation","annotationid","resetForms","userid","entry","hide","val","startcontainer","endcontainer","startoffset","endoffset","annotationstart","annotationend","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","rangeSelectors","map","range","annotation","target","selectors","selector","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","start","end","ajax","url","data","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","addClass","mouseleave","attr","complete","error"],"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, focusannotation, focusgradingform, overwriteannotations) => {\n\n var edited = false;\n var annotations = Array();\n\n var newannotation = false;\n\n // Focus grading form.\n if (focusgradingform) {\n $('.gradingform').addClass('show');\n $('#id_feedback_' + focusgradingform + '_editoreditable').focus();\n }\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', '.margic_entry #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 $('.margic_entry 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=\"annotationstart\"]').val(\n newannotation.target[0].selector[1].start);\n $('.annotation-form-' + entry + ' input[name=\"annotationend\"]').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).css(\"background-color\", 'lightblue');\n });\n\n $('.annotated').mouseleave(function() {\n var id = this.id.replace('annotated-', '');\n $('.annotation-box-' + id).removeClass('hovered');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\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).css(\"background-color\", 'lightblue');\n });\n\n $(document).on('mouseleave', '.hoverannotation', function() {\n var id = this.id.replace('hoverannotation-', '');\n $('.annotated-' + id).css(\"background-color\", $('.annotated-' + id).css('textDecorationColor'));\n });\n\n\n // Focus annotation if needed.\n if (focusannotation != 0) {\n $('.annotated-' + focusannotation).attr('tabindex', -1);\n $('.annotated-' + focusannotation).focus();\n }\n\n },\n complete: function() {\n $('#overlay').hide();\n },\n error: function() {\n // For output: 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.annotationstart),\n end: parseInt(annotation.annotationend)},\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 && (overwriteannotations || 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=\"annotationstart\"]').val(annotations[annotationid].annotationstart);\n $('.annotation-form-' + entry + ' input[name=\"annotationend\"]').val(annotations[annotationid].annotationend);\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":["obj","_jquery","__esModule","default","_exports","init","cmid","canmakeannotations","myuserid","focusannotation","focusgradingform","overwriteannotations","edited","annotations","Array","newannotation","editAnnotation","annotationid","removeAllTempHighlights","resetForms","userid","entry","$","hide","val","startcontainer","endcontainer","startoffset","endoffset","annotationstart","annotationend","exact","prefix","suffix","text","type","html","replaceAll","css","color","insertBefore","show","focus","not","addClass","removeClass","document","on","e","preventDefault","keypress","which","this","parents","submit","window","getSelection","getRangeAt","cloneContents","textContent","root","ranges","collapsed","rangeSelectors","map","range","describe","annotation","target","selectors","selector","anchor","createAnnotation","id","replace","startContainer","endContainer","startOffset","endOffset","start","end","ajax","url","data","getannotations","success","response","JSON","parse","Object","values","parseInt","recreateAnnotations","mouseenter","mouseleave","attr","complete","error"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/colorpicker-layout.min.js b/amd/build/colorpicker-layout.min.js new file mode 100644 index 0000000..8c98d73 --- /dev/null +++ b/amd/build/colorpicker-layout.min.js @@ -0,0 +1,10 @@ +define("mod_margic/colorpicker-layout",["exports","jquery"],(function(_exports,_jquery){var obj; +/** + * Module for layouting custom color picker element as default form element. + * + * @module mod_discourse/colorpicker-layout + * @copyright 2023 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=colorpickerid=>{(0,_jquery.default)(".path-mod-margic .mform fieldset #fitem_"+colorpickerid).addClass("row"),(0,_jquery.default)(".path-mod-margic .mform fieldset #fitem_"+colorpickerid).addClass("form-group"),(0,_jquery.default)(".path-mod-margic .mform fieldset .fitemtitle").addClass("col-md-3"),(0,_jquery.default)(".path-mod-margic .mform fieldset .fitemtitle").addClass("col-form-label"),(0,_jquery.default)(".path-mod-margic .mform fieldset .fitemtitle").addClass("d-flex"),(0,_jquery.default)(".path-mod-margic .mform fieldset .ftext").addClass("col-md-9")}})); + +//# sourceMappingURL=colorpicker-layout.min.js.map \ No newline at end of file diff --git a/amd/build/colorpicker-layout.min.js.map b/amd/build/colorpicker-layout.min.js.map new file mode 100644 index 0000000..684c4fe --- /dev/null +++ b/amd/build/colorpicker-layout.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"colorpicker-layout.min.js","sources":["../src/colorpicker-layout.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 layouting custom color picker element as default form element.\n *\n * @module mod_discourse/colorpicker-layout\n * @copyright 2023 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 = (colorpickerid) => {\n $('.path-mod-margic .mform fieldset #fitem_' + colorpickerid).addClass('row');\n $('.path-mod-margic .mform fieldset #fitem_' + colorpickerid).addClass('form-group');\n\n $('.path-mod-margic .mform fieldset .fitemtitle').addClass('col-md-3');\n $('.path-mod-margic .mform fieldset .fitemtitle').addClass('col-form-label');\n $('.path-mod-margic .mform fieldset .fitemtitle').addClass('d-flex');\n\n $('.path-mod-margic .mform fieldset .ftext').addClass('col-md-9');\n\n};"],"names":["obj","_jquery","__esModule","default","_exports","init","colorpickerid","$","addClass"],"mappings":"wFAuBuB,IAAAA;;;;;;;kFAAvBC,SAAuBD,IAAvBC,UAAuBD,IAAAE,WAAAF,IAAAG,CAAAA,QAAAH,KAYrBI,SAAAC,KAVmBC,iBACjB,EAAAC,QAAAA,SAAE,2CAA6CD,eAAeE,SAAS,QACvE,EAAAD,QAAAA,SAAE,2CAA6CD,eAAeE,SAAS,eAEvE,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,aAC3D,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,mBAC3D,EAAAD,QAAAA,SAAE,gDAAgDC,SAAS,WAE3D,EAAAD,QAAAA,SAAE,2CAA2CC,SAAS,WAAW,CAEnE"} \ No newline at end of file diff --git a/amd/build/highlighting.min.js b/amd/build/highlighting.min.js index 18539b4..b1eae32 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,_types,_textRange){var obj;function highlightRange(range){let annotationid=arguments.length>1&&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";const textNodes=wholeTextNodesInRange(range);let textNodeSpans=[],prevNode=null,currentSpan=null;textNodes.forEach((node=>{prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));const whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((span=>span.some((node=>!whitespace.test(node.nodeValue)))));const highlights=[];return textNodeSpans.forEach((nodes=>{const 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((node=>highlightEl.appendChild(node))),highlights.push(highlightEl)})),highlights}function wholeTextNodesInRange(range){if(range.collapsed)return[];let root=range.commonAncestorContainer;if(root.nodeType!==Node.ELEMENT_NODE&&(root=root.parentElement),!root)return[];const textNodes=[],nodeIter=root.ownerDocument.createNodeIterator(root,NodeFilter.SHOW_TEXT);let node;for(;node=nodeIter.nextNode();){if(!isNodeInRange(range,node))continue;let 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){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return anchor.toRange(options)}function replaceWith(node,replacements){const parent=node.parentNode;replacements.forEach((r=>parent.insertBefore(r,node))),node.remove()}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.anchor=function(annotation,root){const highlight=anchor=>{const range=function(anchor){if(!anchor.range)return null;try{return anchor.range.toRange()}catch{return null}}(anchor);if(!range)return;let highlights=[];highlights=annotation.annotation?highlightRange(range,annotation.annotation.id,"annotated",annotation.annotation.color):highlightRange(range,!1,"annotated_temp"),highlights.forEach((h=>{h._annotation=anchor.annotation})),anchor.highlights=highlights};annotation.target||(annotation.target=[]);const anchors=annotation.target.map((target=>{if(!target.selector||!target.selector.some((s=>"TextQuoteSelector"===s.type)))return{annotation:annotation,target:target};let anchor;try{const range=function(root,selectors){let options=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},position=null,quote=null,range=null;for(let selector of selectors)switch(selector.type){case"TextPositionSelector":position=selector,options.hint=position.start;break;case"TextQuoteSelector":quote=selector;break;case"RangeSelector":range=selector}const maybeAssertQuote=range=>{var _quote;if(null!==(_quote=quote)&&void 0!==_quote&&_quote.exact&&range.toString()!==quote.exact)throw new Error("quote mismatch");return range};let queryselector=!1;try{if(range)return queryselector=querySelector(_types.RangeAnchor.fromSelector(root,range),options),queryselector||maybeAssertQuote}catch(error){try{if(position)return queryselector=querySelector(_types.TextPositionAnchor.fromSelector(root,position),options),queryselector||maybeAssertQuote}catch(error){try{if(quote)return queryselector=querySelector(_types.TextQuoteAnchor.fromSelector(root,quote),options),queryselector}catch(error){return!1}}}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}));for(let anchor of anchors)highlight(anchor);return annotation.$orphan=anchors.length>0&&anchors.every((anchor=>anchor.target.selector&&!anchor.range)),anchors},_exports.describe=function(root,range){const types=[_types.RangeAnchor,_types.TextPositionAnchor,_types.TextQuoteAnchor],result=[];for(let type of types)try{const anchor=type.fromRange(root,range);result.push(anchor.toSelector())}catch(error){continue}return result},_exports.removeAllTempHighlights=function(){const highlights=Array.from((0,_jquery.default)("body")[0].querySelectorAll(".annotated_temp"));void 0!==highlights&&0!=highlights.length&&function(highlights){for(var i=0;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";const textNodes=function(range){if(range.collapsed)return[];let root=range.commonAncestorContainer;root.nodeType!==Node.ELEMENT_NODE&&(root=root.parentElement);if(!root)return[];const textNodes=[],nodeIter=root.ownerDocument.createNodeIterator(root,NodeFilter.SHOW_TEXT);let node;for(;node=nodeIter.nextNode();){if(!isNodeInRange(range,node))continue;let text=node;text===range.startContainer&&range.startOffset>0?text.splitText(range.startOffset):(text===range.endContainer&&range.endOffset{prevNode&&prevNode.nextSibling===node?currentSpan.push(node):(currentSpan=[node],textNodeSpans.push(currentSpan)),prevNode=node}));const whitespace=/^\s*$/;textNodeSpans=textNodeSpans.filter((span=>span.some((node=>!whitespace.test(node.nodeValue)))));const highlights=[];return textNodeSpans.forEach((nodes=>{const 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((node=>highlightEl.appendChild(node))),highlights.push(highlightEl)})),highlights}function isNodeInRange(range,node){try{var _node$nodeValue$lengt,_node$nodeValue;const 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 querySelector(anchor){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return anchor.toRange(options)}function replaceWith(node,replacements){const parent=node.parentNode;replacements.forEach((r=>parent.insertBefore(r,node))),node.remove()}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.anchor=function(annotation,root){const highlight=anchor=>{const range=function(anchor){if(!anchor.range)return null;try{return anchor.range.toRange()}catch{return null}}(anchor);if(!range)return;let highlights=[];highlights=annotation.annotation?highlightRange(range,annotation.annotation.id,"annotated",annotation.annotation.color):highlightRange(range,!1,"annotated_temp"),highlights.forEach((h=>{h._annotation=anchor.annotation})),anchor.highlights=highlights};annotation.target||(annotation.target=[]);const anchors=annotation.target.map((target=>{if(!target.selector||!target.selector.some((s=>"TextQuoteSelector"===s.type)))return{annotation:annotation,target:target};let anchor;try{const range=function(root,selectors){let options=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},position=null,quote=null,range=null;for(let selector of selectors)switch(selector.type){case"TextPositionSelector":position=selector,options.hint=position.start;break;case"TextQuoteSelector":quote=selector;break;case"RangeSelector":range=selector}const maybeAssertQuote=range=>{var _quote;if(null!==(_quote=quote)&&void 0!==_quote&&_quote.exact&&range.toString()!==quote.exact)throw new Error("quote mismatch");return range};let queryselector=!1;try{if(range)return queryselector=querySelector(_types.RangeAnchor.fromSelector(root,range),options),queryselector||maybeAssertQuote}catch(error){try{if(position)return queryselector=querySelector(_types.TextPositionAnchor.fromSelector(root,position),options),queryselector||maybeAssertQuote}catch(error){try{if(quote)return queryselector=querySelector(_types.TextQuoteAnchor.fromSelector(root,quote),options),queryselector}catch(error){return!1}}}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}));for(let anchor of anchors)highlight(anchor);return annotation.$orphan=anchors.length>0&&anchors.every((anchor=>anchor.target.selector&&!anchor.range)),anchors},_exports.describe=function(root,range){const types=[_types.RangeAnchor,_types.TextPositionAnchor,_types.TextQuoteAnchor],result=[];for(let type of types)try{const anchor=type.fromRange(root,range);result.push(anchor.toSelector())}catch(error){continue}return result},_exports.removeAllTempHighlights=function(){const 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 // 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 return false;\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","queryselector","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 return false;\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":["obj","highlightRange","range","annotationid","arguments","length","undefined","cssClass","color","textNodes","collapsed","root","commonAncestorContainer","nodeType","Node","ELEMENT_NODE","parentElement","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","node","nextNode","isNodeInRange","text","startContainer","startOffset","splitText","endContainer","endOffset","data","push","wholeTextNodesInRange","textNodeSpans","prevNode","currentSpan","forEach","nextSibling","whitespace","filter","span","some","test","nodeValue","highlights","nodes","highlightEl","document","createElement","className","style","id","backgroundColor","parentNode","replaceChild","appendChild","_node$nodeValue$lengt","_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","_quote","exact","toString","Error","queryselector","RangeAnchor","fromSelector","error","TextPositionAnchor","TextQuoteAnchor","htmlAnchor","textRange","TextRange","fromRange","err","$orphan","every","types","result","toSelector","Array","from","$","default","querySelectorAll","i","children","removeHighlights","_jquery","__esModule"],"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 b560094..b583708 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":["/**\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,eAGrBC,SAAW,EACXC,aAAe,SACE,IAAdD,UACLA,SAAWH,KAAKK,QAAQJ,IAAKE,WACX,IAAdA,WACFC,aAAaE,KAAK,CAChBC,MAAOJ,SACPK,IAAKL,SAAWF,IAAIQ,OACpBC,OAAQ,IAEVP,UAAY,UAGZC,aAAaK,OAAS,EACjBL,cAKF,wBAAaJ,KAAMC,IAAKC,oBAUxBS,eAAeX,KAAMC,QAIT,IAAfA,IAAIQ,QAAgC,IAAhBT,KAAKS,cACpB,SAMF,EAHSV,OAAOC,KAAMC,IAAKA,IAAIQ,QAGlB,GAAGC,OAAST,IAAIQ,4FAkBXT,KAAMY,WAAOC,+DAAU,MAC3B,IAAjBD,MAAMH,cACD,WAYHP,UAAYY,KAAKC,IAAI,IAAKH,MAAMH,OAAS,GAGzCO,QAAUjB,OAAOC,KAAMY,MAAOV,cAEb,IAAnBc,QAAQP,cACH,WASHQ,WAAaC,cAMXC,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,MAEAC,SAAW,KACa,iBAAjBb,QAAQc,KAAmB,CAEpCD,SAAW,EADIZ,KAAKc,IAAIV,MAAMX,MAAQM,QAAQc,MACpB3B,KAAKS,cA1Bb,GA8BJU,WA7BK,GA8BJC,YA7BI,GA8BJI,YA7BC,EA8BJE,UACGG,IAQbC,cAAgBd,QAAQe,KAAIC,KAChCzB,MAAOyB,EAAEzB,MACTC,IAAKwB,EAAExB,IACPyB,MAAOhB,WAAWe,cAIpBF,cAAcI,MAAK,CAACC,EAAGC,IAAMA,EAAEH,MAAQE,EAAEF,QAClCH,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":["obj","search","text","str","maxErrors","matchPos","exactMatches","indexOf","push","start","end","length","errors","approxSearch","textMatchScore","quote","context","arguments","undefined","Math","min","matches","scoreMatch","match","quoteScore","prefixScore","prefix","slice","max","suffixScore","suffix","posScore","hint","abs","quoteWeight","scoredMatches","map","m","score","sort","a","b","_stringMatch","__esModule","default"],"mappings":"8FAQ0C,IAAAA,IAuB1C,SAASC,OAAOC,KAAMC,IAAKC,WAGzB,IAAIC,SAAW,EACXC,aAAe,GACnB,MAAqB,IAAdD,UACLA,SAAWH,KAAKK,QAAQJ,IAAKE,WACX,IAAdA,WACFC,aAAaE,KAAK,CAChBC,MAAOJ,SACPK,IAAKL,SAAWF,IAAIQ,OACpBC,OAAQ,IAEVP,UAAY,GAGhB,OAAIC,aAAaK,OAAS,EACjBL,cAKF,EAAAO,sBAAaX,KAAMC,IAAKC,UACjC,CASA,SAASU,eAAeZ,KAAMC,KAI5B,GAAmB,IAAfA,IAAIQ,QAAgC,IAAhBT,KAAKS,OAC3B,OAAO,EAMT,OAAO,EAHSV,OAAOC,KAAMC,IAAKA,IAAIQ,QAGlB,GAAGC,OAAST,IAAIQ,MACtC,6EAiBO,SAAoBT,KAAMa,OAAqB,IAAdC,QAAOC,UAAAN,OAAA,QAAAO,IAAAD,UAAA,GAAAA,UAAA,GAAG,CAAA,EAChD,GAAqB,IAAjBF,MAAMJ,OACR,OAAO,KAYT,MAAMP,UAAYe,KAAKC,IAAI,IAAKL,MAAMJ,OAAS,GAGzCU,QAAUpB,OAAOC,KAAMa,MAAOX,WAEpC,GAAuB,IAAnBiB,QAAQV,OACV,OAAO,KAST,MAAMW,WAAaC,QACjB,MAKMC,WAAa,EAAID,MAAMX,OAASG,MAAMJ,OAEtCc,YAAcT,QAAQU,OACxBZ,eACEZ,KAAKyB,MACHR,KAAKS,IAAI,EAAGL,MAAMd,MAAQO,QAAQU,OAAOf,QACzCY,MAAMd,OAERO,QAAQU,QAEV,EACEG,YAAcb,QAAQc,OACxBhB,eACEZ,KAAKyB,MAAMJ,MAAMb,IAAKa,MAAMb,IAAMM,QAAQc,OAAOnB,QACjDK,QAAQc,QAEV,EAEJ,IAAIC,SAAW,EACf,GAA4B,iBAAjBf,QAAQgB,KAAmB,CAEpCD,SAAW,EADIZ,KAAKc,IAAIV,MAAMd,MAAQO,QAAQgB,MACpB9B,KAAKS,MACjC,CAUA,OArCoB,GA8BJa,WA7BK,GA8BJC,YA7BI,GA8BJI,YA7BC,EA8BJE,UACGG,EAGK,EAKlBC,cAAgBd,QAAQe,KAAIC,IAAM,CACtC5B,MAAO4B,EAAE5B,MACTC,IAAK2B,EAAE3B,IACP4B,MAAOhB,WAAWe,OAKpB,OADAF,cAAcI,MAAK,CAACC,EAAGC,IAAMA,EAAEH,MAAQE,EAAEF,QAClCH,cAAc,EACvB,EArKAO,cAA0C1C,IAA1C0C,eAA0C1C,IAAA2C,WAAA3C,IAAA4C,CAAAA,QAAA5C,IAqKzC"} \ 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 a413799..943986e 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","_exports","default"],"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 44708c6..20b72c0 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 * @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","nextOffset","shift","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","results","textNode","currentNode","nextNode","undefined","data","push","offset","RangeError","TextPosition","constructor","Error","relativeTo","parent","contains","this","el","parentElement","resolve","options","err","direction","tw","document","createTreeWalker","getRootNode","forwards","text","previousNode","fromPoint","textOffset","childNodes","i","TextRange","start","end","toRange","range","Range","setStart","setEnd","startContainer","startOffset","endContainer","endOffset","root"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"text-range.min.js","sources":["../src/text-range.js"],"sourcesContent":["/**\n * Functions for handling text-ranges used by the other methods.\n *\n * This code originaly is from the Hypothesis project (https://github.com/hypothesis/client)\n * which is released under the 2-Clause BSD License (https://opensource.org/licenses/BSD-2-Clause),\n * sometimes referred to as the \"Simplified BSD License\".\n */\n\n/**\n * Return the combined length of text nodes contained in `node`.\n *\n * @param {Node} node\n * @return {string}\n */\nfunction nodeTextLength(node) {\n switch (node.nodeType) {\n case Node.ELEMENT_NODE:\n case Node.TEXT_NODE:\n // Nb. `textContent` excludes text in comments and processing instructions\n // when called on a parent element, so we don't need to subtract that here.\n\n return /** @type {string} */ (node.textContent).length;\n default:\n return 0;\n }\n}\n\n/**\n * Return the total length of the text of all previous siblings of `node`.\n *\n * @param {Node} node\n * @return {int}\n */\nfunction previousSiblingsTextLength(node) {\n let sibling = node.previousSibling;\n let length = 0;\n\n while (sibling) {\n length += nodeTextLength(sibling);\n sibling = sibling.previousSibling;\n }\n\n return length;\n}\n\n/**\n * Resolve one or more character offsets within an element to (text node, position)\n * pairs.\n *\n * @param {Element} element\n * @param {number[]} offsets - Offsets, which must be sorted in ascending order\n * @return {{ node: Text, offset: number }[]}\n */\nfunction resolveOffsets(element, ...offsets) {\n\n let nextOffset = offsets.shift();\n const nodeIter = /** @type {Document} */ (\n element.ownerDocument\n ).createNodeIterator(element, NodeFilter.SHOW_TEXT);\n const results = [];\n\n let currentNode = nodeIter.nextNode();\n let textNode;\n let length = 0;\n\n // Find the text node containing the `nextOffset`th character from the start\n // of `element`.\n while (nextOffset !== undefined && currentNode) {\n textNode = /** @type {Text} */ (currentNode);\n\n if (length + textNode.data.length > nextOffset) {\n results.push({node: textNode, offset: nextOffset - length});\n nextOffset = offsets.shift();\n } else {\n currentNode = nodeIter.nextNode();\n length += textNode.data.length;\n }\n }\n\n // Boundary case.\n while (nextOffset !== undefined && length === nextOffset) {\n results.push({node: textNode, offset: textNode.data.length});\n nextOffset = offsets.shift();\n }\n\n if (nextOffset !== undefined) {\n throw new RangeError('Offset exceeds text length');\n }\n\n return results;\n}\n\nexport let RESOLVE_FORWARDS = 1;\nexport let RESOLVE_BACKWARDS = 2;\n\n/**\n * Represents an offset within the text content of an element.\n *\n * This position can be resolved to a specific descendant node in the current\n * DOM subtree of the element using the `resolve` method.\n */\nexport class TextPosition {\n /**\n * Construct a `TextPosition` that refers to the text position `offset` within\n * the text content of `element`.\n *\n * @param {Element} element\n * @param {number} offset\n */\n constructor(element, offset) {\n if (offset < 0) {\n throw new Error('Offset is invalid');\n }\n\n /** Element that `offset` is relative to. */\n this.element = element;\n\n /** Character offset from the start of the element's `textContent`. */\n this.offset = offset;\n }\n\n /**\n * Return a copy of this position with offset relative to a given ancestor\n * element.\n *\n * @param {Element} parent - Ancestor of `this.element`\n * @return {TextPosition}\n */\n relativeTo(parent) {\n if (!parent.contains(this.element)) {\n throw new Error('Parent is not an ancestor of current element');\n }\n\n let el = this.element;\n let offset = this.offset;\n while (el !== parent) {\n offset += previousSiblingsTextLength(el);\n el = /** @type {Element} */ (el.parentElement);\n }\n\n return new TextPosition(el, offset);\n }\n\n /**\n * Resolve the position to a specific text node and offset within that node.\n *\n * Throws if `this.offset` exceeds the length of the element's text. In the\n * case where the element has no text and `this.offset` is 0, the `direction`\n * option determines what happens.\n *\n * Offsets at the boundary between two nodes are resolved to the start of the\n * node that begins at the boundary.\n *\n * @param {Object} [options]\n * @param {RESOLVE_FORWARDS|RESOLVE_BACKWARDS} [options.direction] -\n * Specifies in which direction to search for the nearest text node if\n * `this.offset` is `0` and `this.element` has no text. If not specified\n * an error is thrown.\n * @return {{ node: Text, offset: number }}\n * @throws {RangeError}\n */\n resolve(options = {}) {\n try {\n return resolveOffsets(this.element, this.offset)[0];\n } catch (err) {\n if (this.offset === 0 && options.direction !== undefined) {\n const tw = document.createTreeWalker(\n this.element.getRootNode(),\n NodeFilter.SHOW_TEXT\n );\n tw.currentNode = this.element;\n const forwards = options.direction === RESOLVE_FORWARDS;\n const text = /** @type {Text|null} */ (\n forwards ? tw.nextNode() : tw.previousNode()\n );\n if (!text) {\n throw err;\n }\n return {node: text, offset: forwards ? 0 : text.data.length};\n } else {\n throw err;\n }\n }\n }\n\n /**\n * Construct a `TextPosition` that refers to the `offset`th character within\n * `node`.\n *\n * @param {Node} node\n * @param {number} offset\n * @return {TextPosition}\n */\n static fromCharOffset(node, offset) {\n switch (node.nodeType) {\n case Node.TEXT_NODE:\n return TextPosition.fromPoint(node, offset);\n case Node.ELEMENT_NODE:\n return new TextPosition(/** @type {Element} */ (node), offset);\n default:\n throw new Error('Node is not an element or text node');\n }\n }\n\n /**\n * Construct a `TextPosition` representing the range start or end point (node, offset).\n *\n * @param {Node} node - Text or Element node\n * @param {number} offset - Offset within the node.\n * @return {TextPosition}\n */\n static fromPoint(node, offset) {\n\n switch (node.nodeType) {\n case Node.TEXT_NODE: {\n if (offset < 0 || offset > /** @type {Text} */ (node).data.length) {\n throw new Error('Text node offset is out of range');\n }\n\n if (!node.parentElement) {\n throw new Error('Text node has no parent');\n }\n\n // Get the offset from the start of the parent element.\n const textOffset = previousSiblingsTextLength(node) + offset;\n\n return new TextPosition(node.parentElement, textOffset);\n }\n case Node.ELEMENT_NODE: {\n if (offset < 0 || offset > node.childNodes.length) {\n throw new Error('Child node offset is out of range');\n }\n\n // Get the text length before the `offset`th child of element.\n let textOffset = 0;\n for (let i = 0; i < offset; i++) {\n textOffset += nodeTextLength(node.childNodes[i]);\n }\n\n return new TextPosition(/** @type {Element} */ (node), textOffset);\n }\n default:\n throw new Error('Point is not in an element or text node');\n }\n }\n}\n\n/**\n * Represents a region of a document as a (start, end) pair of `TextPosition` points.\n *\n * Representing a range in this way allows for changes in the DOM content of the\n * range which don't affect its text content, without affecting the text content\n * of the range itself.\n */\nexport class TextRange {\n /**\n * Construct an immutable `TextRange` from a `start` and `end` point.\n *\n * @param {TextPosition} start\n * @param {TextPosition} end\n */\n constructor(start, end) {\n this.start = start;\n this.end = end;\n }\n\n /**\n * Return a copy of this range with start and end positions relative to a\n * given ancestor. See `TextPosition.relativeTo`.\n *\n * @param {Element} element\n * @return {Range}\n */\n relativeTo(element) {\n return new TextRange(\n this.start.relativeTo(element),\n this.end.relativeTo(element)\n );\n }\n\n /**\n * Resolve the `TextRange` to a DOM range.\n *\n * The resulting DOM Range will always start and end in a `Text` node.\n * Hence `TextRange.fromRange(range).toRange()` can be used to \"shrink\" a\n * range to the text it contains.\n *\n * May throw if the `start` or `end` positions cannot be resolved to a range.\n *\n * @return {Range}\n */\n toRange() {\n let start;\n let end;\n\n if (\n this.start.element === this.end.element &&\n this.start.offset <= this.end.offset\n ) {\n // Fast path for start and end points in same element.\n [start, end] = resolveOffsets(\n this.start.element,\n this.start.offset,\n this.end.offset\n );\n } else {\n start = this.start.resolve({direction: RESOLVE_FORWARDS});\n end = this.end.resolve({direction: RESOLVE_BACKWARDS});\n }\n\n const range = new Range();\n range.setStart(start.node, start.offset);\n range.setEnd(end.node, end.offset);\n return range;\n }\n\n /**\n * Convert an existing DOM `Range` to a `TextRange`\n *\n * @param {Range} range\n * @return {TextRange}\n */\n static fromRange(range) {\n const start = TextPosition.fromPoint(\n range.startContainer,\n range.startOffset\n );\n const end = TextPosition.fromPoint(range.endContainer, range.endOffset);\n return new TextRange(start, end);\n }\n\n /**\n * Return a `TextRange` from the `start`th to `end`th characters in `root`.\n *\n * @param {Element} root\n * @param {number} start\n * @param {number} end\n * @return {Range}\n */\n static fromOffsets(root, start, end) {\n return new TextRange(\n new TextPosition(root, start),\n new TextPosition(root, end)\n );\n }\n}\n"],"names":["nodeTextLength","node","nodeType","Node","ELEMENT_NODE","TEXT_NODE","textContent","length","previousSiblingsTextLength","sibling","previousSibling","resolveOffsets","element","_len","arguments","offsets","Array","_key","nextOffset","shift","nodeIter","ownerDocument","createNodeIterator","NodeFilter","SHOW_TEXT","results","textNode","currentNode","nextNode","undefined","data","push","offset","RangeError","_exports","RESOLVE_FORWARDS","RESOLVE_BACKWARDS","TextPosition","constructor","Error","this","relativeTo","parent","contains","el","parentElement","resolve","options","err","direction","tw","document","createTreeWalker","getRootNode","forwards","text","previousNode","static","fromPoint","textOffset","childNodes","i","TextRange","start","end","toRange","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 dab80b0..51b6a10 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","constructor","root","range","selector","startContainer","Error","endContainer","startPos","TextPosition","fromCharOffset","startOffset","endPos","endOffset","TextRange","toRange","this","toSelector","normalizedRange","fromRange","textRange","start","element","end","type","offset","TextPositionAnchor","relativeTo","fromOffsets","TextQuoteAnchor","exact","context","text","textContent","slice","prefix","Math","max","suffix","min","length","options","toPositionAnchor","match","hint"],"mappings":"gRA2BaA,YAKXC,YAAYC,KAAMC,YACXD,KAAOA,UACPC,MAAQA,uBAQED,KAAMC,cACd,IAAIH,YAAYE,KAAMC,2BAUXD,KAAME,gBAElBC,gBAAiB,wBAAcD,SAASC,eAAgBH,UAEzDG,qBACG,IAAIC,MAAM,gDAGZC,cAAe,wBAAcH,SAASG,aAAcL,UACrDK,mBACG,IAAID,MAAM,8CAGZE,SAAWC,wBAAaC,eAC5BL,eACAD,SAASO,aAELC,OAASH,wBAAaC,eAC1BH,aACAH,SAASS,WAGLV,MAAQ,IAAIW,qBAAUN,SAAUI,QAAQG,iBACvC,IAAIf,YAAYE,KAAMC,OAG/BY,iBACSC,KAAKb,MAMdc,mBAIQC,gBAAkBJ,qBAAUK,UAAUH,KAAKb,OAAOY,UAElDK,UAAYN,qBAAUK,UAAUD,iBAChCb,gBAAiB,wBAAce,UAAUC,MAAMC,QAASN,KAAKd,MAC7DK,cAAe,wBAAca,UAAUG,IAAID,QAASN,KAAKd,YAExD,CACLsB,KAAM,gBACNnB,eAAAA,eACAM,YAAaS,UAAUC,MAAMI,OAC7BlB,aAAAA,aACAM,UAAWO,UAAUG,IAAIE,gDAQlBC,mBAMXzB,YAAYC,KAAMmB,MAAOE,UAClBrB,KAAOA,UACPmB,MAAQA,WACRE,IAAMA,qBAQIrB,KAAMC,aACfiB,UAAYN,qBAAUK,UAAUhB,OAAOwB,WAAWzB,aACjD,IAAIwB,mBACTxB,KACAkB,UAAUC,MAAMI,OAChBL,UAAUG,IAAIE,4BAQEvB,KAAME,iBACjB,IAAIsB,mBAAmBxB,KAAME,SAASiB,MAAOjB,SAASmB,KAM/DN,mBACS,CACLO,KAAM,uBACNH,MAAOL,KAAKK,MACZE,IAAKP,KAAKO,KAIdR,iBACSD,qBAAUc,YAAYZ,KAAKd,KAAMc,KAAKK,MAAOL,KAAKO,KAAKR,gEAYrDc,gBAQX5B,YAAYC,KAAM4B,WAAOC,+DAAU,QAC5B7B,KAAOA,UACP4B,MAAQA,WACRC,QAAUA,yBAYA7B,KAAMC,aACf6B,KAA8B9B,KAAK+B,YACnCb,UAAYN,qBAAUK,UAAUhB,OAAOwB,WAAWzB,MAElDmB,MAAQD,UAAUC,MAAMI,OACxBF,IAAMH,UAAUG,IAAIE,cAanB,IAAII,gBAAgB3B,KAAM8B,KAAKE,MAAMb,MAAOE,KAAM,CACvDY,OAAQH,KAAKE,MAAME,KAAKC,IAAI,EAAGhB,MAHd,IAGmCA,OACpDiB,OAAQN,KAAKE,MAAMX,IAAKa,KAAKG,IAAIP,KAAKQ,OAAQjB,IAJ7B,2BAaDrB,KAAME,gBAClB+B,OAACA,OAADG,OAASA,QAAUlC,gBAClB,IAAIyB,gBAAgB3B,KAAME,SAAS0B,MAAO,CAACK,OAAAA,OAAQG,OAAAA,SAM5DrB,mBACS,CACLO,KAAM,oBACNM,MAAOd,KAAKc,MACZK,OAAQnB,KAAKe,QAAQI,OACrBG,OAAQtB,KAAKe,QAAQO,QAQzBvB,cAAQ0B,+DAAU,UACTzB,KAAK0B,iBAAiBD,SAAS1B,UAOxC2B,uBAAiBD,+DAAU,SACnBT,KAA8BhB,KAAKd,KAAK+B,YACxCU,OAAQ,0BAAWX,KAAMhB,KAAKc,MAAO,IACtCd,KAAKe,QACRa,KAAMH,QAAQG,WAGXD,YACG,IAAIrC,MAAM,0BAGX,IAAIoB,mBAAmBV,KAAKd,KAAMyC,MAAMtB,MAAOsB,MAAMpB"} \ 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","constructor","root","range","this","static","selector","startContainer","nodeFromXPath","Error","endContainer","startPos","TextPosition","fromCharOffset","startOffset","endPos","endOffset","TextRange","toRange","toSelector","normalizedRange","fromRange","textRange","xpathFromNode","start","element","end","type","offset","_exports","TextPositionAnchor","relativeTo","fromOffsets","TextQuoteAnchor","exact","context","arguments","length","undefined","text","textContent","slice","prefix","Math","max","suffix","min","options","toPositionAnchor","match","matchQuote","hint"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/xpath.min.js b/amd/build/xpath.min.js index fb26f06..4cd3bb3 100644 --- a/amd/build/xpath.min.js +++ b/amd/build/xpath.min.js @@ -1,3 +1,3 @@ -define("mod_margic/xpath",["exports"],(function(_exports){function getPathSegment(node){const name=function(node){const nodeName=node.nodeName.toLowerCase();let result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){let pos=0,tmp=node;for(;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();let matchIndex=-1;for(let 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}},_exports.xpathFromNode=function(node,root){let xpath="",elem=node;for(;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=xpath.replace(/\/$/,""),xpath}})); +define("mod_margic/xpath",["exports"],(function(_exports){function getPathSegment(node){const name=function(node){const nodeName=node.nodeName.toLowerCase();let result=nodeName;return"#text"===nodeName&&(result="text()"),result}(node),pos=function(node){let pos=0,tmp=node;for(;tmp;)tmp.nodeName===node.nodeName&&(pos+=1),tmp=tmp.previousSibling;return pos}(node);return"".concat(name,"[").concat(pos,"]")}function nthChildOfType(element,nodeName,index){nodeName=nodeName.toUpperCase();let matchIndex=-1;for(let i=0;i1&&void 0!==arguments[1]?arguments[1]:document.body;try{return function(xpath,root){const isSimpleXPath=null!==xpath.match(/^(\/[A-Za-z0-9-]+(\[[0-9]+\])?)+$/);if(!isSimpleXPath)throw new Error("Expression is not a simple XPath");const segments=xpath.split("/");let element=root;segments.shift();for(let segment of segments){let elementName,elementIndex;const separatorPos=segment.indexOf("[");if(-1!==separatorPos){elementName=segment.slice(0,separatorPos);const indexStr=segment.slice(separatorPos+1,segment.indexOf("]"));if(elementIndex=parseInt(indexStr)-1,elementIndex<0)return null}else elementName=segment,elementIndex=0;const child=nthChildOfType(element,elementName,elementIndex);if(!child)return null;element=child}return element}(xpath,root)}catch(err){return document.evaluate("."+xpath,root,null,XPathResult.FIRST_ORDERED_NODE_TYPE,null).singleNodeValue}},_exports.xpathFromNode=function(node,root){let xpath="",elem=node;for(;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=xpath.replace(/\/$/,""),xpath}})); //# sourceMappingURL=xpath.min.js.map \ No newline at end of file diff --git a/amd/build/xpath.min.js.map b/amd/build/xpath.min.js.map index 6317652..21ed853 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 /** @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":"mEAgDSA,eAAeC,YAChBC,cAnCaD,YACbE,SAAWF,KAAKE,SAASC,kBAC3BC,OAASF,eACI,UAAbA,WACFE,OAAS,UAEJA,OA6BMC,CAAYL,MACnBM,aArBiBN,UACnBM,IAAM,EAENC,IAAMP,UACHO,KACDA,IAAIL,WAAaF,KAAKE,WACxBI,KAAO,GAETC,IAAMA,IAAIC,uBAELF,IAWKG,CAAgBT,sBAClBC,iBAAQK,kBAsCXI,eAAeC,QAAST,SAAUU,OACzCV,SAAWA,SAASW,kBAEhBC,YAAc,MACb,IAAIC,EAAI,EAAGA,EAAIJ,QAAQK,SAASC,OAAQF,IAAK,OAC1CG,MAAQP,QAAQK,SAASD,MAC3BG,MAAMhB,SAASW,gBAAkBX,aACjCY,WACEA,aAAeF,cACVM,aAKN,cAwBAC,oBAAoBC,MAAOC,WAEqB,OAArDD,MAAME,MAAM,4CAEN,IAAIC,MAAM,0CAGZC,SAAWJ,MAAMK,MAAM,SACzBd,QAAUU,KAIdG,SAASE,YAEJ,IAAIC,WAAWH,SAAU,KACxBI,YACAC,mBAEEC,aAAeH,QAAQI,QAAQ,SACf,IAAlBD,aAAqB,CACvBF,YAAcD,QAAQK,MAAM,EAAGF,oBAEzBG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,SACjEF,aAAeK,SAASD,UAAY,EAChCJ,aAAe,SACV,UAGTD,YAAcD,QACdE,aAAe,QAGXX,MAAQR,eAAeC,QAASiB,YAAaC,kBAC9CX,aACI,KAGTP,QAAUO,aAGLP,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,kBAEdvB,MAAQ,IAAMA,MACdA,MAAQA,MAAMwB,QAAQ,MAAO,IAEtBxB"} \ 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","concat","nthChildOfType","element","index","toUpperCase","matchIndex","i","children","length","child","xpath","root","arguments","undefined","document","body","isSimpleXPath","match","Error","segments","split","shift","segment","elementName","elementIndex","separatorPos","indexOf","slice","indexStr","parseInt","evaluateSimpleXPath","err","evaluate","XPathResult","FIRST_ORDERED_NODE_TYPE","singleNodeValue","elem","parentNode","replace"],"mappings":"0DAgDA,SAASA,eAAeC,MACtB,MAAMC,KAnCR,SAAqBD,MACnB,MAAME,SAAWF,KAAKE,SAASC,cAC/B,IAAIC,OAASF,SAIb,MAHiB,UAAbA,WACFE,OAAS,UAEJA,MACT,CA4BeC,CAAYL,MACnBM,IArBR,SAAyBN,MACvB,IAAIM,IAAM,EAENC,IAAMP,KACV,KAAOO,KACDA,IAAIL,WAAaF,KAAKE,WACxBI,KAAO,GAETC,IAAMA,IAAIC,gBAEZ,OAAOF,GACT,CAUcG,CAAgBT,MAC5B,MAAA,GAAAU,OAAUT,KAAIS,KAAAA,OAAIJ,IAAG,IACvB,CAqCA,SAASK,eAAeC,QAASV,SAAUW,OACzCX,SAAWA,SAASY,cAEpB,IAAIC,YAAc,EAClB,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,QAAQK,SAASC,OAAQF,IAAK,CAChD,MAAMG,MAAQP,QAAQK,SAASD,GAC/B,GAAIG,MAAMjB,SAASY,gBAAkBZ,aACjCa,WACEA,aAAeF,OACjB,OAAOM,KAGb,CAEA,OAAO,IACT,gFA4EO,SAAuBC,OAA6B,IAAtBC,KAAIC,UAAAJ,OAAAI,QAAAC,IAAAD,UAAAC,GAAAD,UAAGE,GAAAA,SAASC,KACnD,IACE,OAvDJ,SAA6BL,MAAOC,MAClC,MAAMK,cACiD,OAArDN,MAAMO,MAAM,qCACd,IAAKD,cACH,MAAM,IAAIE,MAAM,oCAGlB,MAAMC,SAAWT,MAAMU,MAAM,KAC7B,IAAIlB,QAAUS,KAIdQ,SAASE,QAET,IAAK,IAAIC,WAAWH,SAAU,CAC5B,IAAII,YACAC,aAEJ,MAAMC,aAAeH,QAAQI,QAAQ,KACrC,IAAsB,IAAlBD,aAAqB,CACvBF,YAAcD,QAAQK,MAAM,EAAGF,cAE/B,MAAMG,SAAWN,QAAQK,MAAMF,aAAe,EAAGH,QAAQI,QAAQ,MAEjE,GADAF,aAAeK,SAASD,UAAY,EAChCJ,aAAe,EACjB,OAAO,IAEX,MACED,YAAcD,QACdE,aAAe,EAGjB,MAAMf,MAAQR,eAAeC,QAASqB,YAAaC,cACnD,IAAKf,MACH,OAAO,KAGTP,QAAUO,KACZ,CAEA,OAAOP,OACT,CAcW4B,CAAoBpB,MAAOC,KACnC,CAAC,MAAOoB,KACP,OAAOjB,SAASkB,SACd,IAAMtB,MACNC,KAGA,KACAsB,YAAYC,wBACZ,MACAC,eACJ,CACF,yBApIO,SAAuB7C,KAAMqB,MAClC,IAAID,MAAQ,GAGR0B,KAAO9C,KACX,KAAO8C,OAASzB,MAAM,CACpB,IAAKyB,KACH,MAAM,IAAIlB,MAAM,oCAElBR,MAAQrB,eAAe+C,MAAQ,IAAM1B,MACrC0B,KAAOA,KAAKC,UACd,CAIA,OAHA3B,MAAQ,IAAMA,MACdA,MAAQA,MAAM4B,QAAQ,MAAO,IAEtB5B,KACT,CAoHC"} \ No newline at end of file diff --git a/amd/src/annotations.js b/amd/src/annotations.js index 7900473..e5be49b 100644 --- a/amd/src/annotations.js +++ b/amd/src/annotations.js @@ -24,13 +24,19 @@ import $ from 'jquery'; import {removeAllTempHighlights, anchor, describe} from './highlighting'; -export const init = (cmid, canmakeannotations, myuserid, focusannotation) => { +export const init = (cmid, canmakeannotations, myuserid, focusannotation, focusgradingform, overwriteannotations) => { var edited = false; var annotations = Array(); var newannotation = false; + // Focus grading form. + if (focusgradingform) { + $('.gradingform').addClass('show'); + $('#id_feedback_' + focusgradingform + '_editoreditable').focus(); + } + // 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'); @@ -216,7 +222,7 @@ export const init = (cmid, canmakeannotations, myuserid, focusannotation) => { removeAllTempHighlights(); // Remove other temporary highlights. resetForms(); // Remove old form contents. edited = false; - } else if (canmakeannotations && myuserid == annotations[annotationid].userid) { + } else if (canmakeannotations && (overwriteannotations || myuserid == annotations[annotationid].userid)) { removeAllTempHighlights(); // Remove other temporary highlights. resetForms(); // Remove old form contents. diff --git a/amd/src/colorpicker-layout.js b/amd/src/colorpicker-layout.js new file mode 100644 index 0000000..355b065 --- /dev/null +++ b/amd/src/colorpicker-layout.js @@ -0,0 +1,36 @@ +// 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 . + +/** + * Module for layouting custom color picker element as default form element. + * + * @module mod_discourse/colorpicker-layout + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import $ from 'jquery'; + +export const init = (colorpickerid) => { + $('.path-mod-margic .mform fieldset #fitem_' + colorpickerid).addClass('row'); + $('.path-mod-margic .mform fieldset #fitem_' + colorpickerid).addClass('form-group'); + + $('.path-mod-margic .mform fieldset .fitemtitle').addClass('col-md-3'); + $('.path-mod-margic .mform fieldset .fitemtitle').addClass('col-form-label'); + $('.path-mod-margic .mform fieldset .fitemtitle').addClass('d-flex'); + + $('.path-mod-margic .mform fieldset .ftext').addClass('col-md-9'); + +}; \ No newline at end of file diff --git a/annotation_form.php b/annotation_form.php index 022c5f9..51d1e0d 100644 --- a/annotation_form.php +++ b/annotation_form.php @@ -105,6 +105,6 @@ public function definition() { * @return array Array with errors */ public function validation($data, $files) { - return array(); + return []; } } diff --git a/annotations.php b/annotations.php index ad5c2d7..bbd128e 100644 --- a/annotations.php +++ b/annotations.php @@ -60,9 +60,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -74,7 +72,7 @@ if ($annotations) { echo json_encode($annotations); } else { - echo json_encode(array()); + echo json_encode([]); } die; @@ -83,10 +81,10 @@ require_capability('mod/margic:makeannotations', $context); // Header. -$PAGE->set_url('/mod/margic/annotations.php', array('id' => $id)); +$PAGE->set_url('/mod/margic/annotations.php', ['id' => $id]); $PAGE->set_title(format_string($moduleinstance->name)); -$urlparams = array('id' => $id, 'annotationmode' => 1); +$urlparams = ['id' => $id, 'annotationmode' => 1]; $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); @@ -96,17 +94,13 @@ global $USER; - if ($DB->record_exists('margic_annotations', array('id' => $deleteannotation, 'margic' => $moduleinstance->id, - 'userid' => $USER->id))) { + $a = $DB->get_record('margic_annotations', ['id' => $deleteannotation, 'margic' => $moduleinstance->id]); + if (isset($a) && ($moduleinstance->overwriteannotations || $a->userid == $USER->id)) { - $DB->delete_records('margic_annotations', array('id' => $deleteannotation, 'margic' => $moduleinstance->id, - 'userid' => $USER->id)); + $DB->delete_records('margic_annotations', ['id' => $deleteannotation, 'margic' => $moduleinstance->id]); // Trigger module annotation deleted event. - $event = \mod_margic\event\annotation_deleted::create(array( - 'objectid' => $deleteannotation, - 'context' => $context - )); + $event = \mod_margic\event\annotation_deleted::create(['objectid' => $deleteannotation, 'context' => $context]); $event->trigger(); @@ -120,19 +114,19 @@ require_once($CFG->dirroot . '/mod/margic/annotation_form.php'); // Instantiate form. -$mform = new mod_margic_annotation_form(null, array('types' => $margic->get_errortypes_for_form())); +$mform = new mod_margic_annotation_form(null, ['types' => $margic->get_errortypes_for_form()]); if ($fromform = $mform->get_data()) { // In this case you process validated data. $mform->get_data() returns data posted in form. if ((isset($fromform->annotationid) && $fromform->annotationid !== 0) && isset($fromform->text)) { // Update annotation. - $annotation = $DB->get_record('margic_annotations', array('margic' => $cm->instance, 'entry' => $fromform->entry, - 'id' => $fromform->annotationid)); + $annotation = $DB->get_record('margic_annotations', ['margic' => $cm->instance, 'entry' => $fromform->entry, + 'id' => $fromform->annotationid, ]); // Prevent changes by user in hidden form fields. if (!$annotation) { redirect($redirecturl, get_string('annotationinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); - } else if ($annotation->userid != $USER->id) { + } else if (!$moduleinstance->overwriteannotations && $annotation->userid != $USER->id) { redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); } @@ -141,20 +135,21 @@ } $annotation->timemodified = time(); - $annotation->text = format_text($fromform->text, 2, array('para' => false)); + $annotation->text = format_text($fromform->text, 2, ['para' => false]); $annotation->type = $fromform->type; + if ($moduleinstance->overwriteannotations) { + $annotation->userid = $USER->id; + } + $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 = \mod_margic\event\annotation_updated::create(['objectid' => $fromform->annotationid, 'context' => $context]); $event->trigger(); - $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $fromform->annotationid); + $urlparams = ['id' => $id, 'annotationmode' => 1, 'focusannotation' => $fromform->annotationid]; $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); redirect($redirecturl, get_string('annotationedited', 'mod_margic'), null, notification::NOTIFY_SUCCESS); @@ -173,7 +168,7 @@ redirect($redirecturl, get_string('annotationinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); } - if (!$DB->record_exists('margic_entries', array('margic' => $cm->instance, 'id' => $fromform->entry))) { + if (!$DB->record_exists('margic_entries', ['margic' => $cm->instance, 'id' => $fromform->entry])) { redirect($redirecturl, get_string('annotationinvalid', 'mod_margic'), null, notification::NOTIFY_ERROR); } @@ -197,13 +192,10 @@ $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 = \mod_margic\event\annotation_created::create(['objectid' => $newid, 'context' => $context]); $event->trigger(); - $urlparams = array('id' => $id, 'annotationmode' => 1, 'focusannotation' => $newid); + $urlparams = ['id' => $id, 'annotationmode' => 1, 'focusannotation' => $newid]; $redirecturl = new moodle_url('/mod/margic/view.php', $urlparams); redirect($redirecturl, get_string('annotationadded', 'mod_margic'), null, notification::NOTIFY_SUCCESS); diff --git a/backup/moodle2/backup_margic_stepslib.php b/backup/moodle2/backup_margic_stepslib.php index cd4e88b..a7a7520 100644 --- a/backup/moodle2/backup_margic_stepslib.php +++ b/backup/moodle2/backup_margic_stepslib.php @@ -37,30 +37,30 @@ protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); // Replace with the attributes and final elements that the element will handle. - $margic = new backup_nested_element('margic', array('id'), array( + $margic = new backup_nested_element('margic', ['id'], [ 'name', 'intro', 'introformat', 'timecreated', 'timemodified', 'scale', 'assessed', 'assesstimestart', 'assesstimefinish', - 'timeopen', 'timeclose', 'editentries', 'editentrydates', 'annotationareawidth')); + 'timeopen', 'timeclose', 'editentries', 'editentrydates', 'annotationareawidth', ]); $errortypes = new backup_nested_element('errortypes'); - $errortype = new backup_nested_element('errortype', array('id'), array( - 'timecreated', 'timemodified', 'name', 'color', 'priority')); + $errortype = new backup_nested_element('errortype', ['id'], [ + 'timecreated', 'timemodified', 'name', 'color', 'priority', ]); $entries = new backup_nested_element('entries'); - $entry = new backup_nested_element('entry', array('id'), array( + $entry = new backup_nested_element('entry', ['id'], [ 'userid', 'timecreated', 'timemodified', 'text', 'format', 'rating', 'feedback', 'formatfeedback', 'teacher', - 'timemarked', 'baseentry')); + 'timemarked', 'baseentry', ]); $annotations = new backup_nested_element('annotations'); - $annotation = new backup_nested_element('annotation', array('id'), array( + $annotation = new backup_nested_element('annotation', ['id'], [ 'userid', 'timecreated', 'timemodified', 'type', 'startcontainer', 'endcontainer', - 'startoffset', 'endoffset', 'annotationstart', 'annotationend', 'exact', 'prefix', 'suffix', 'text')); + 'startoffset', 'endoffset', 'annotationstart', 'annotationend', 'exact', 'prefix', 'suffix', 'text', ]); $ratings = new backup_nested_element('ratings'); - $rating = new backup_nested_element('rating', array('id'), array( + $rating = new backup_nested_element('rating', ['id'], [ 'component', 'ratingarea', 'scaleid', 'value', 'userid', - 'timecreated', 'timemodified')); + 'timecreated', 'timemodified', ]); // Build the tree with these elements with $margic as the root of the backup tree. $margic->add_child($errortypes); @@ -77,24 +77,24 @@ protected function define_structure() { // Define the source tables for the elements. - $margic->set_source_table('margic', array('id' => backup::VAR_ACTIVITYID)); + $margic->set_source_table('margic', ['id' => backup::VAR_ACTIVITYID]); // Errortypes. - $errortype->set_source_table('margic_errortypes', array('margic' => backup::VAR_PARENTID)); + $errortype->set_source_table('margic_errortypes', ['margic' => backup::VAR_PARENTID]); if ($userinfo) { // Entries. - $entry->set_source_table('margic_entries', array('margic' => backup::VAR_PARENTID)); + $entry->set_source_table('margic_entries', ['margic' => backup::VAR_PARENTID]); // Annotations. - $annotation->set_source_table('margic_annotations', array('entry' => backup::VAR_PARENTID)); + $annotation->set_source_table('margic_annotations', ['entry' => backup::VAR_PARENTID]); // Ratings (core). - $rating->set_source_table('rating', array('contextid' => backup::VAR_CONTEXTID, + $rating->set_source_table('rating', ['contextid' => backup::VAR_CONTEXTID, 'itemid' => backup::VAR_PARENTID, 'component' => backup_helper::is_sqlparam('mod_margic'), - 'ratingarea' => backup_helper::is_sqlparam('entry'))); + 'ratingarea' => backup_helper::is_sqlparam('entry'), ]); $rating->set_source_alias('rating', 'value'); } diff --git a/backup/moodle2/restore_margic_activity_task.class.php b/backup/moodle2/restore_margic_activity_task.class.php index d22b8f0..eab052a 100644 --- a/backup/moodle2/restore_margic_activity_task.class.php +++ b/backup/moodle2/restore_margic_activity_task.class.php @@ -53,12 +53,12 @@ protected function define_my_steps() { * @return array. */ public static function define_decode_contents() { - $contents = array(); + $contents = []; // 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'); + // tablename, [field1, field 2], $mapping. + $contents[] = new restore_decode_content('margic', ['intro'], 'margic'); + $contents[] = new restore_decode_content('margic_entries', ['text', 'feedback'], 'margic_entry'); return $contents; } @@ -69,16 +69,16 @@ public static function define_decode_contents() { * @return array. */ public static function define_decode_rules() { - $rules = array(); + $rules = []; // Define the rules. $rules[] = new restore_decode_rule('MARGICINDEX', '/mod/margic/index.php?id=$1', 'course'); $rules[] = new restore_decode_rule('MARGICVIEWBYID', '/mod/margic/view.php?id=$1&userid=$2', - array('course_module', 'userid')); - $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1', array('course_module')); + ['course_module', 'userid']); + $rules[] = new restore_decode_rule('MARGICEDITVIEW', '/mod/margic/edit.php?id=$1', ['course_module']); $rules[] = new restore_decode_rule('MARGICANNOTATIONSUMMARY', '/mod/margic/error_summary.php?id=$1', 'course_module'); - $rules[] = new restore_decode_rule('MARGICERRORTYPES', '/mod/margic/errortypes.php?id=$1', array('course_module')); + $rules[] = new restore_decode_rule('MARGICERRORTYPES', '/mod/margic/errortypes.php?id=$1', ['course_module']); return $rules; } @@ -91,7 +91,7 @@ public static function define_decode_rules() { * @return array. */ public static function define_restore_log_rules() { - $rules = array(); + $rules = []; // Define the rules. $rules[] = new restore_log_rule('margic', 'view', 'view.php?id={course_module}', '{margic}'); @@ -117,7 +117,7 @@ public static function define_restore_log_rules() { * activity level. All them are rules not linked to any module instance (cmid = 0) */ public static function define_restore_log_rules_for_course() { - $rules = array(); + $rules = []; $rules[] = new restore_log_rule('margic', 'view all', 'index.php?id={course}', null); diff --git a/backup/moodle2/restore_margic_stepslib.php b/backup/moodle2/restore_margic_stepslib.php index 8be8fff..3ee9d89 100644 --- a/backup/moodle2/restore_margic_stepslib.php +++ b/backup/moodle2/restore_margic_stepslib.php @@ -36,7 +36,7 @@ class restore_margic_activity_structure_step extends restore_activity_structure_ * @return restore_path_element[]. */ protected function define_structure() { - $paths = array(); + $paths = []; $userinfo = $this->get_setting_value('userinfo'); diff --git a/classes/event/annotation_created.php b/classes/event/annotation_created.php index 0049944..08b47fb 100644 --- a/classes/event/annotation_created.php +++ b/classes/event/annotation_created.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_annotations', 'restore' => 'margic_annotation'); + return ['db' => 'margic_annotations', 'restore' => 'margic_annotation']; } } diff --git a/classes/event/annotation_deleted.php b/classes/event/annotation_deleted.php index da1f750..1ae0546 100644 --- a/classes/event/annotation_deleted.php +++ b/classes/event/annotation_deleted.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_annotations', 'restore' => 'margic_annotation'); + return ['db' => 'margic_annotations', 'restore' => 'margic_annotation']; } } diff --git a/classes/event/annotation_updated.php b/classes/event/annotation_updated.php index b0d99b3..5fb8299 100644 --- a/classes/event/annotation_updated.php +++ b/classes/event/annotation_updated.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_annotations', 'restore' => 'margic_annotation'); + return ['db' => 'margic_annotations', 'restore' => 'margic_annotation']; } } diff --git a/classes/event/course_module_viewed.php b/classes/event/course_module_viewed.php index e814d9a..3dbaa16 100644 --- a/classes/event/course_module_viewed.php +++ b/classes/event/course_module_viewed.php @@ -47,6 +47,6 @@ protected function init() { * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic', 'restore' => 'margic'); + return ['db' => 'margic', 'restore' => 'margic']; } } diff --git a/classes/event/download_margic_entries.php b/classes/event/download_margic_entries.php index 7518ba1..90c0308 100644 --- a/classes/event/download_margic_entries.php +++ b/classes/event/download_margic_entries.php @@ -66,8 +66,6 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } } diff --git a/classes/event/entry_created.php b/classes/event/entry_created.php index 044dcd7..703053c 100644 --- a/classes/event/entry_created.php +++ b/classes/event/entry_created.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/edit.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/edit.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_entries', 'restore' => 'margic_entry'); + return ['db' => 'margic_entries', 'restore' => 'margic_entry']; } } diff --git a/classes/event/entry_updated.php b/classes/event/entry_updated.php index 5367513..78eb44d 100644 --- a/classes/event/entry_updated.php +++ b/classes/event/entry_updated.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/edit.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/edit.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_entries', 'restore' => 'margic_entry'); + return ['db' => 'margic_entries', 'restore' => 'margic_entry']; } } diff --git a/classes/event/feedback_updated.php b/classes/event/feedback_updated.php index 24c6f29..7ca7a78 100644 --- a/classes/event/feedback_updated.php +++ b/classes/event/feedback_updated.php @@ -66,15 +66,13 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $this->contextinstanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } /** * Get objectid mapping for restore. */ public static function get_objectid_mapping() { - return array('db' => 'margic_entries', 'restore' => 'margic_entry'); + return ['db' => 'margic_entries', 'restore' => 'margic_entry']; } } diff --git a/classes/event/invalid_access_attempt.php b/classes/event/invalid_access_attempt.php index 28d2232..0f0e2bb 100644 --- a/classes/event/invalid_access_attempt.php +++ b/classes/event/invalid_access_attempt.php @@ -68,6 +68,6 @@ public function get_description() { * @return \moodle_url */ public function get_url() { - return new \moodle_url('/mod/margic/view.php', array('id' => $this->contextinstanceid)); + return new \moodle_url('/mod/margic/view.php', ['id' => $this->contextinstanceid]); } } diff --git a/classes/forms/mod_margic_colorpicker_form_element.php b/classes/forms/mod_margic_colorpicker_form_element.php new file mode 100644 index 0000000..6efc76b --- /dev/null +++ b/classes/forms/mod_margic_colorpicker_form_element.php @@ -0,0 +1,163 @@ +. + +/** + * Color picker custom form element. + * + * @package mod_margic + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once("HTML/QuickForm/text.php"); + +/** + * Color picker custom form element. + * + * @package mod_margic + * @copyright 2023 coactum GmbH + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_margic_colorpicker_form_element extends HTML_QuickForm_text { + /** @var forceltr Whether to force the display of this element to flow LTR. */ + public $forceltr = false; + + /** @var _helpbutton String html for help button, if empty then no help. */ + public $_helpbutton = ''; + + /** @var _hiddenlabel If true label will be hidden. */ + public $_hiddenlabel = false; + + /** + * Constructor. + * + * @param string $elementname (optional) name of the text field + * @param string $elementlabel (optional) text field label + * @param string $attributes (optional) Either a typical HTML attribute string or an associative array + */ + public function __construct($elementname = null, $elementlabel = null, $attributes = null) { + parent::__construct($elementname, $elementlabel, $attributes); + parent::setSize(30); + parent::setMaxlength(7); + } + + /** + * Sets label to be hidden. + * + * @param bool $hiddenlabel sets if label should be hidden + */ + public function sethiddenlabel($hiddenlabel) { + $this->_hiddenlabel = $hiddenlabel; + } + + /** + * Freeze the element so that only its value is returned and set persistantfreeze to false. + * + * @return void + */ + public function freeze() { + $this->_flagFrozen = true; + // No hidden element is needed refer MDL-30845. + $this->setPersistantFreeze(false); + } + + /** + * Returns the html to be used when the element is frozen. + * + * @return string Frozen html + */ + public function getfrozenhtml() { + $attributes = ['readonly' => 'readonly']; + $this->updateAttributes($attributes); + return $this->_getTabs() . '_getAttrString($this->_attributes) . '/>' . $this->_getPersistantData(); + } + + /** + * Returns HTML for this form element. + * + * @return string + */ + public function tohtml() { + global $CFG, $PAGE; + + $PAGE->requires->js_init_call('M.util.init_colour_picker', ['id_color', null]); + $PAGE->requires->js_call_amd('mod_margic/colorpicker-layout', 'init', ['id_color']); + + // Add the class at the last minute. + if ($this->get_force_ltr()) { + if (!isset($this->_attributes['class'])) { + $this->_attributes['class'] = 'form-control text-ltr'; + } else { + $this->_attributes['class'] .= 'form-control text-ltr'; + } + } + + $this->_generateId(); + if ($this->_flagFrozen) { + return $this->getfrozenhtml(); + } + + $html = $this->_getTabs() . + '
+
+
+
+
+ _getAttrString($this->_attributes) . '"> + +
+
+
'; + + if ($this->_hiddenlabel) { + return '' . $html; + } else { + return $html; + } + } + + /** + * Get html for help button. + * + * @return string html for help button + */ + public function gethelpbutton() { + return $this->_helpbutton; + } + + /** + * Get force LTR option. + * + * @return bool + */ + public function get_force_ltr() { + return $this->forceltr; + } + + /** + * Force the field to flow left-to-right. + * + * This is useful for fields such as URLs, passwords, settings, etc... + * + * @param bool $value The value to set the option to. + */ + public function set_force_ltr($value) { + $this->forceltr = (bool) $value; + } +} diff --git a/classes/local/entrystats.php b/classes/local/entrystats.php index 80bc8a5..4486e44 100644 --- a/classes/local/entrystats.php +++ b/classes/local/entrystats.php @@ -46,7 +46,7 @@ public static function get_entry_stats($entrytext, $entrytimecreated) { $cleantext = preg_replace('#<[^>]+>#', ' ', $entrytext, -1, $replacementspacescount); - $entrystats = array(); + $entrystats = []; $entrystats['words'] = self::get_stats_words($cleantext); $entrystats['chars'] = self::get_stats_chars($cleantext) - $replacementspacescount; $entrystats['sentences'] = self::get_stats_sentences($cleantext); diff --git a/classes/local/helper.php b/classes/local/helper.php index 4f1c476..a7a2c04 100644 --- a/classes/local/helper.php +++ b/classes/local/helper.php @@ -67,11 +67,11 @@ public static function margic_update_calendar(stdClass $margic, $cmid) { // The MOOTYPER_EVENT_TYPE_OPEN event should only be an action event if no close time is specified. $event->type = empty($margic->timeclose) ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; - if ($event->id = $DB->get_field('event', 'id', array( + if ($event->id = $DB->get_field('event', 'id', [ 'modulename' => 'margic', 'instance' => $margic->id, - 'eventtype' => $event->eventtype - ))) { + 'eventtype' => $event->eventtype, + ])) { if ((! empty($margic->timeopen)) && ($margic->timeopen > 0)) { // Calendar event exists so update it. @@ -112,11 +112,11 @@ public static function margic_update_calendar(stdClass $margic, $cmid) { $event = new stdClass(); $event->type = CALENDAR_EVENT_TYPE_ACTION; $event->eventtype = MARGIC_EVENT_TYPE_CLOSE; - if ($event->id = $DB->get_field('event', 'id', array( + if ($event->id = $DB->get_field('event', 'id', [ 'modulename' => 'margic', 'instance' => $margic->id, - 'eventtype' => $event->eventtype - ))) { + 'eventtype' => $event->eventtype, + ])) { if ((! empty($margic->timeclose)) && ($margic->timeclose > 0)) { // Calendar event exists so update it. $event->name = get_string('calendarend', 'margic', $margic->name); @@ -180,13 +180,6 @@ public static function download_entries($context, $course, $margic) { $data = new stdClass(); $data->margic = $margic->id; - // Trigger download_margic_entries event. - $event = \mod_margic\event\download_margic_entries::create(array( - 'objectid' => $data->margic, - 'context' => $context - )); - $event->trigger(); - // Construct sql query and filename based on admin, teacher, or student. // Add filename details based on course and margic activity name. $csv = new csv_export_writer(); @@ -210,8 +203,8 @@ public static function download_entries($context, $course, $margic) { } $csv->filename .= '_'.clean_filename(gmdate("Ymd_Hi").'GMT.csv'); - $fields = array(); - $fields = array( + $fields = []; + $fields = [ get_string('id', 'margic'), get_string('firstname'), get_string('lastname'), @@ -225,8 +218,8 @@ public static function download_entries($context, $course, $margic) { get_string('teacher', 'margic'), get_string('timemarked', 'margic'), get_string('baseentry', 'margic'), - get_string('text', 'margic') - ); + get_string('text', 'margic'), + ]; // Add the headings to our data array. $csv->add_data($fields); @@ -274,7 +267,7 @@ public static function download_entries($context, $course, $margic) { $timemarked = date('Y-m-d H:i:s', $m->timemarked); } - $output = array( + $output = [ $m->entry, $m->firstname, $m->lastname, @@ -288,8 +281,8 @@ public static function download_entries($context, $course, $margic) { $m->teacher, $timemarked, $m->baseentry, - format_text($m->text, $m->format, array('para' => false)) - ); + format_text($m->text, $m->format, ['para' => false]), + ]; $csv->add_data($output); } } @@ -310,7 +303,7 @@ public static function margic_get_editor_and_attachment_options($course, $contex $maxbytes = $course->maxbytes; // For the editor. - $editoroptions = array( + $editoroptions = [ 'trusttext' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes, @@ -318,22 +311,22 @@ public static function margic_get_editor_and_attachment_options($course, $contex 'subdirs' => false, 'editentrydates' => $margic->editentrydates, // Custom data (not really for editor). - ); + ]; // 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). - $attachmentoptions = array( + $attachmentoptions = [ 'subdirs' => false, 'maxfiles' => 1, - 'maxbytes' => $maxbytes - ); + 'maxbytes' => $maxbytes, + ]; - return array( + return [ $editoroptions, - $attachmentoptions - ); + $attachmentoptions, + ]; } /** @@ -382,7 +375,7 @@ public static function margic_get_edittime_options($moduleinstance) { */ public static function check_rating_entry($ratingoptions) { global $DB, $CFG; - $params = array(); + $params = []; $params['contextid'] = $ratingoptions->contextid; $params['component'] = $ratingoptions->component; $params['ratingarea'] = $ratingoptions->ratingarea; @@ -450,9 +443,10 @@ public static function get_margic_aggregation($aggregate) { * @param object $entry * @param array $grades * @param bool $canmanageentries + * @param bool $sendgradingmessage */ public static function margic_return_feedback_area_for_entry($cmid, $context, $course, $margic, $entry, $grades, - $canmanageentries) { + $canmanageentries, $sendgradingmessage) { $grade = false; @@ -464,20 +458,23 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c require_once(__DIR__ .'/../../../../lib/gradelib.php'); if ($entry->teacher) { - $teacher = $DB->get_record('user', array('id' => $entry->teacher)); + $teacher = $DB->get_record('user', ['id' => $entry->teacher]); if ($teacher) { $teacherimage = $OUTPUT->user_picture($teacher, - array('courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 30)); + ['courseid' => $course->id, 'link' => true, 'includefullname' => true, 'size' => 30]); + $hasteacher = true; } else { $teacherimage = false; + $hasteacher = false; } } else { $teacherimage = false; + $hasteacher = false; } $feedbackarea = ''; - $feedbacktext = format_text($entry->feedback, $entry->formatfeedback, array('para' => false)); + $feedbacktext = format_text($entry->feedback, $entry->formatfeedback, ['para' => false]); if ($canmanageentries) { // If user is teacher. if (! $entry->teacher) { @@ -495,6 +492,7 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $data->timecreated = $entry->timecreated; $data->{'feedback_' . $entry->id} = $entry->feedback; $data->{'feedback_' . $entry->id . 'format'} = $entry->formatfeedback; + $data->sendgradingmessage = $sendgradingmessage; list ($editoroptions, $attachmentoptions) = self::margic_get_editor_and_attachment_options($course, $context, $margic); @@ -507,8 +505,9 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $data->{'rating_' . $entry->id} = $entry->rating; $mform = new \mod_margic_grading_form(new \moodle_url('/mod/margic/grade_entry.php', - array('id' => $cmid, 'entryid' => $entry->id)), array('courseid' => $course->id, 'margic' => $margic, - 'entry' => $entry, 'grades' => $grades, 'teacherimg' => $teacherimage, 'editoroptions' => $editoroptions)); + ['id' => $cmid, 'entryid' => $entry->id]), ['courseid' => $course->id, 'margic' => $margic, + 'entry' => $entry, 'grades' => $grades, 'teacherimg' => $teacherimage, 'editoroptions' => $editoroptions, + 'hasteacher' => $hasteacher, ]); // Set default data. $mform->set_data($data); @@ -518,18 +517,19 @@ public static function margic_return_feedback_area_for_entry($cmid, $context, $c $feedbackarea .= '
'; $feedbackarea .= '
' . get_string('feedback') . ' ' . get_string('from', 'mod_margic') . ' ' . $teacherimage . ' '; - $feedbackarea .= get_string('at', 'mod_margic') . ' ' . userdate($entry->timemarked) . ''; - $feedbackarea .= ''; + if ($entry->timemarked) { + $feedbackarea .= get_string('at', 'mod_margic') . ' ' . userdate($entry->timemarked); + } + + $feedbackarea .= ''; if ($margic->assessed > 0) { // Gradebook preference. - $gradinginfo = grade_get_grades($course->id, 'mod', 'margic', $entry->margic, array( - $entry->userid - )); + $gradinginfo = grade_get_grades($course->id, 'mod', 'margic', $entry->margic, [$entry->userid]); // Branch check for string compatibility. - if (! empty($grades)) { + if (! empty($grades) && $entry->rating) { if ($CFG->branch > 310) { $feedbackarea .= get_string('gradenoun') . ': '; } else { diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 260aa9b..b2a2fe3 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -80,7 +80,7 @@ public static function get_metadata(collection $items): collection { 'text' => 'privacy:metadata:margic_annotations:text', ], 'privacy:metadata:margic_annotations'); - // The table 'margic_errortype_templates' stores the errirtype templatess of all margics. + // The table 'margic_errortype_templates' stores the errortype templates of all margics. $items->add_database_table('margic_errortype_templates', [ 'timecreated' => 'privacy:metadata:margic_errortype_templates:timecreated', 'timemodified' => 'privacy:metadata:margic_errortype_templates:timemodified', @@ -114,7 +114,7 @@ public static function get_contexts_for_userid(int $userid): contextlist { $params = [ 'modulename' => 'margic', 'contextlevel' => CONTEXT_MODULE, - 'userid' => $userid + 'userid' => $userid, ]; // Get contexts for entries. @@ -431,7 +431,7 @@ protected static function export_annotation_data(int $userid, \context $context, 'timecreated' => transform::datetime($annotation->timecreated), 'timemodified' => $timemodified, 'type' => $annotation->type, - 'text' => format_text($annotation->text, 2, array('para' => false)), + 'text' => format_text($annotation->text, 2, ['para' => false]), ]; // Store the annotation data. diff --git a/classes/search/activity.php b/classes/search/activity.php index 2eb42d7..0f399c9 100644 --- a/classes/search/activity.php +++ b/classes/search/activity.php @@ -48,11 +48,11 @@ public function uses_file_indexing() { * @return array */ public function get_search_fileareas() { - $fileareas = array( + $fileareas = [ 'intro', 'entry', - 'feedback' - ); // Fileareas. + 'feedback', + ]; // Fileareas. return $fileareas; } } diff --git a/classes/search/entry.php b/classes/search/entry.php index 63a2c9a..a68d2f2 100644 --- a/classes/search/entry.php +++ b/classes/search/entry.php @@ -41,7 +41,7 @@ class entry extends \core_search\base_mod { * * @var array Internal quick static cache. */ - protected $entriesdata = array(); + protected $entriesdata = []; /** * Returns recordset containing required data for indexing margic entries. @@ -65,7 +65,7 @@ public function get_document_recordset($modifiedfrom = 0, \context $context = nu WHERE me.timemodified >= :timemodified ORDER BY me.timemodified ASC"; return $DB->get_recordset_sql($sql, array_merge($contextparams, [ - 'timemodified' => $modifiedfrom + 'timemodified' => $modifiedfrom, ])); } @@ -76,7 +76,7 @@ public function get_document_recordset($modifiedfrom = 0, \context $context = nu * @param array $options * @return \core_search\document */ - public function get_document($entry, $options = array()) { + public function get_document($entry, $options = []) { try { $cm = $this->get_cm('margic', $entry->margic, $entry->course); $context = \context_module::instance($cm->id); @@ -156,9 +156,7 @@ public function get_doc_url(\core_search\document $doc) { $entryuserid = $doc->get('userid'); $url = '/mod/margic/view.php'; - return new \moodle_url($url, array( - 'id' => $contextmodule->instanceid - )); + return new \moodle_url($url, ['id' => $contextmodule->instanceid]); } /** @@ -169,9 +167,7 @@ public function get_doc_url(\core_search\document $doc) { */ public function get_context_url(\core_search\document $doc) { $contextmodule = \context::instance_by_id($doc->get('contextid')); - return new \moodle_url('/mod/margic/view.php', array( - 'id' => $contextmodule->instanceid - )); + return new \moodle_url('/mod/margic/view.php', ['id' => $contextmodule->instanceid]); } /** @@ -187,6 +183,6 @@ protected function get_entry($entryid) { global $DB; 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); + WHERE me.id = ?", ['id' => $entryid], MUST_EXIST); } } diff --git a/db/access.php b/db/access.php index 58be0c3..317c722 100644 --- a/db/access.php +++ b/db/access.php @@ -23,139 +23,175 @@ */ defined('MOODLE_INTERNAL') || die(); -$capabilities = array( +$capabilities = [ - 'mod/margic:addinstance' => array( + 'mod/margic:addinstance' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, - 'archetypes' => array( + 'archetypes' => [ 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ), - 'clonepermissionsfrom' => 'moodle/course:manageactivities' - ), + 'manager' => CAP_ALLOW, + ], + 'clonepermissionsfrom' => 'moodle/course:manageactivities', + ], - 'mod/margic:manageentries' => array( + 'mod/margic:manageentries' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM | RISK_PERSONAL, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:rate' => array( + 'mod/margic:rate' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:addentries' => array( + 'mod/margic:addentries' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:makeannotations' => array( + 'mod/margic:makeannotations' => [ 'riskbitmask' => RISK_XSS | RISK_SPAM, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:deleteannotations' => array( + 'mod/margic:deleteannotations' => [ 'riskbitmask' => RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:viewannotations' => array( + 'mod/margic:viewannotations' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:manageerrortypes' => array( + 'mod/margic:manageerrortypes' => [ 'riskbitmask' => RISK_XSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:viewerrorsummary' => array( + 'mod/margic:viewerrorsummary' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:viewerrorsfromallparticipants' => array( + 'mod/margic:viewerrorsfromallparticipants' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:editdefaulterrortypes' => array( + 'mod/margic:editdefaulterrortypes' => [ 'riskbitmask' => RISK_XSS | RISK_DATALOSS, 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( - 'manager' => CAP_ALLOW - ) - ), + 'archetypes' => [ + 'manager' => CAP_ALLOW, + ], + ], - 'mod/margic:receivegradingmessages' => array( + 'mod/margic:receivegradingmessages' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, - 'archetypes' => array( + 'archetypes' => [ 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, - 'manager' => CAP_ALLOW - ) - ), -); + 'manager' => CAP_ALLOW, + ], + ], + + 'mod/margic:viewotherusersentrytimes' => [ + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], + + 'mod/margic:viewotherusersannotationtimes' => [ + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], + + 'mod/margic:viewotherusersfeedbacktimes' => [ + 'riskbitmask' => RISK_PERSONAL, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => [ + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ], + ], +]; diff --git a/db/install.php b/db/install.php index 81686a1..44770f5 100644 --- a/db/install.php +++ b/db/install.php @@ -36,7 +36,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_verb'; - $errortype->color = '0040FF'; + $errortype->color = '6EB0FC'; $errortype->defaulttype = 1; $errortype->userid = 0; @@ -47,7 +47,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_syntax'; - $errortype->color = '0080FF'; + $errortype->color = 'BAB2FD'; $errortype->defaulttype = 1; $errortype->userid = 0; @@ -58,7 +58,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'grammar_congruence'; - $errortype->color = '0489B1'; + $errortype->color = '04CDD2'; $errortype->defaulttype = 1; $errortype->userid = 0; @@ -91,7 +91,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'orthography'; - $errortype->color = 'DF0101'; + $errortype->color = 'FF571E'; $errortype->defaulttype = 1; $errortype->userid = 0; @@ -102,7 +102,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'punctuation'; - $errortype->color = 'FFFF00'; + $errortype->color = 'F7D358'; $errortype->defaulttype = 1; $errortype->userid = 0; @@ -113,7 +113,7 @@ function xmldb_margic_install() { $errortype->timecreated = time(); $errortype->timemodified = 0; $errortype->name = 'other'; - $errortype->color = 'F7D358'; + $errortype->color = 'FFFF00'; $errortype->defaulttype = 1; $errortype->userid = 0; diff --git a/db/install.xml b/db/install.xml index 5ceaad8..b41573c 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,5 +1,5 @@ - @@ -21,7 +21,9 @@ + + diff --git a/db/messages.php b/db/messages.php index fb480cb..c742e5f 100644 --- a/db/messages.php +++ b/db/messages.php @@ -25,13 +25,12 @@ defined('MOODLE_INTERNAL') || die(); -$messageproviders = array ( - - 'gradingmessages' => array( +$messageproviders = [ + 'gradingmessages' => [ 'capability' => 'mod/margic:receivegradingmessages', - 'defaults' => array( + 'defaults' => [ 'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF, 'email' => MESSAGE_PERMITTED, - ), - ), -); + ], + ], +]; diff --git a/db/upgrade.php b/db/upgrade.php index 7208171..1e305be 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -44,5 +44,26 @@ function xmldb_margic_upgrade($oldversion) { } upgrade_mod_savepoint(true, 2023030700, 'margic'); } + + if ($oldversion < 2023100300) { // Added column for default value for send grading messages. + $table = new xmldb_table('margic'); + $field = new xmldb_field('sendgradingmessage', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'editentrydates'); + + // Conditionally launch add field for table. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + $field = new xmldb_field('overwriteannotations', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', + 'annotationareawidth'); + + // Conditionally launch add field for table. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + upgrade_mod_savepoint(true, 2023100300, 'margic'); + } + return true; } diff --git a/edit.php b/edit.php index 5dd5c3b..5faf017 100644 --- a/edit.php +++ b/edit.php @@ -23,7 +23,7 @@ */ use mod_margic\local\helper; -use \mod_margic\event\invalid_access_attempt; +use mod_margic\event\invalid_access_attempt; use core\output\notification; use mod_margic\output\margic_entry; @@ -57,9 +57,7 @@ throw new moodle_exception(get_string('incorrectcourseid', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -70,13 +68,11 @@ // Prevent creating and editing of entries if user is not allowed to edit entry or activity is not available. if (($entryid && !$moduleinstance->editentries) || !helper::margic_available($moduleinstance)) { // Trigger invalid_access_attempt with redirect to the view page. - $params = array( + $params = [ 'objectid' => $id, 'context' => $context, - 'other' => array( - 'file' => 'edit.php' - ) - ); + 'other' => ['file' => 'edit.php'], + ]; $event = invalid_access_attempt::create($params); $event->trigger(); redirect('view.php?id='.$id, get_string('editentrynotpossible', 'margic'), null, notification::NOTIFY_ERROR); @@ -93,20 +89,20 @@ $data->id = $cm->id; // Get the single record specified by firstkey. -if ($DB->record_exists('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid))) { - $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entryid)); +if ($DB->record_exists('margic_entries', ['margic' => $moduleinstance->id, "id" => $entryid])) { + $entry = $DB->get_record('margic_entries', ['margic' => $moduleinstance->id, "id" => $entryid]); $notnewestentry = false; // Prevent editing of entries that are not the newest version of a base entry or a unedited entry. if (isset($entry->baseentry)) { // If entry has a base entry check if this entry is the newest childentry. $otherchildentries = $DB->get_records('margic_entries', - array('margic' => $moduleinstance->id, 'baseentry' => $entry->baseentry), 'timecreated DESC'); + ['margic' => $moduleinstance->id, 'baseentry' => $entry->baseentry], 'timecreated DESC'); if ($entry->timecreated < $otherchildentries[array_key_first($otherchildentries)]->timecreated) { $notnewestentry = true; } } else { // If this entry has no base entry check if it has childentries and cant therefore be edited. - $childentries = $DB->get_records('margic_entries', array('margic' => $moduleinstance->id, 'baseentry' => $entry->id), + $childentries = $DB->get_records('margic_entries', ['margic' => $moduleinstance->id, 'baseentry' => $entry->id], 'timecreated DESC'); if (!empty($childentries)) { @@ -117,13 +113,11 @@ // Prevent editing of entries not started by this user or if it is not the newest child entry. if ($entry->userid != $USER->id || $notnewestentry) { // Trigger invalid_access_attempt with redirect to the view page. - $params = array( + $params = [ 'objectid' => $id, 'context' => $context, - 'other' => array( - 'file' => 'edit.php' - ) - ); + 'other' => ['file' => 'edit.php'], + ]; $event = invalid_access_attempt::create($params); $event->trigger(); redirect('view.php?id='.$id, get_string('editentrynotpossible', 'margic'), null, notification::NOTIFY_ERROR); @@ -135,7 +129,7 @@ $data->textformat = $entry->format; $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', - array('cmid' => $cm->id, 'canmakeannotations' => false, 'myuserid' => $USER->id)); + ['cmid' => $cm->id, 'canmakeannotations' => false, 'myuserid' => $USER->id]); } else { $entry = false; @@ -153,7 +147,7 @@ // Create form. $form = new mod_margic_entry_form(null, - array('editentrydates' => $moduleinstance->editentrydates, 'editoroptions' => $editoroptions)); + ['editentrydates' => $moduleinstance->editentrydates, 'editoroptions' => $editoroptions]); // Set existing data for this entry. $form->set_data($data); @@ -190,7 +184,7 @@ // 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)); + $baseentry = $DB->get_record('margic_entries', ['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'), @@ -198,7 +192,7 @@ } $connectedentries = $DB->get_records('margic_entries', - array('margic' => $moduleinstance->id, 'baseentry' => $newentry->baseentry), 'timecreated DESC'); + ['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'), @@ -208,7 +202,7 @@ } // Update timemodified for base entry. - $baseentry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $newentry->baseentry)); + $baseentry = $DB->get_record('margic_entries', ['margic' => $moduleinstance->id, "id" => $newentry->baseentry]); $baseentry->timemodified = $newentry->timecreated; $DB->update_record('margic_entries', $baseentry); } @@ -223,23 +217,23 @@ $entrytext = file_rewrite_pluginfile_urls($fromform->text, 'pluginfile.php', $context->id, 'mod_margic', 'entry', $newentry->id); - $newentry->text = format_text($entrytext, $fromform->textformat, array('para' => false)); + $newentry->text = format_text($entrytext, $fromform->textformat, ['para' => false]); $newentry->format = $fromform->textformat; $DB->update_record('margic_entries', $newentry); if ($entry && $fromform->entryid) { // Trigger module entry updated event. - $event = \mod_margic\event\entry_updated::create(array( + $event = \mod_margic\event\entry_updated::create([ 'objectid' => $newentry->id, - 'context' => $context - )); + 'context' => $context, + ]); } else { // Trigger module entry created event. - $event = \mod_margic\event\entry_created::create(array( + $event = \mod_margic\event\entry_created::create([ 'objectid' => $newentry->id, - 'context' => $context - )); + 'context' => $context, + ]); } $event->trigger(); @@ -254,7 +248,7 @@ } -$PAGE->set_url('/mod/margic/edit.php', array('id' => $id)); +$PAGE->set_url('/mod/margic/edit.php', ['id' => $id]); $PAGE->navbar->add($title); $PAGE->set_title(format_string($moduleinstance->name) . ' - ' . $title); $PAGE->set_heading($course->fullname); @@ -295,7 +289,7 @@ $regradingstr = get_string('needsregrading', 'margic'); if ($entry->baseentry) { // If edited entry is child entry get base entry for rendering. - $entry = $DB->get_record('margic_entries', array('margic' => $moduleinstance->id, "id" => $entry->baseentry)); + $entry = $DB->get_record('margic_entries', ['margic' => $moduleinstance->id, "id" => $entry->baseentry]); } $page = new margic_entry($margic, $cm, $context, $moduleinstance, $entry, $margic->get_annotationarea_width(), diff --git a/error_summary.php b/error_summary.php index 6e1c722..e5ace10 100644 --- a/error_summary.php +++ b/error_summary.php @@ -67,9 +67,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -86,16 +84,24 @@ if ($addtomargic && $manageerrortypes) { require_sesskey(); - $redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/margic/error_summary.php', ['id' => $id]); - if ($DB->record_exists('margic_errortype_templates', array('id' => $addtomargic))) { + if ($DB->record_exists('margic_errortype_templates', ['id' => $addtomargic])) { global $USER; - $type = $DB->get_record('margic_errortype_templates', array('id' => $addtomargic)); + $type = $DB->get_record('margic_errortype_templates', ['id' => $addtomargic]); if ($type->defaulttype == 1 || ($type->defaulttype == 0 && $type->userid == $USER->id)) { - $type->priority = count($margic->get_margic_errortypes()) + 1; + + $etypes = $margic->get_margic_errortypes(); + + if ($etypes) { + $type->priority = $etypes[array_key_last($etypes)]->priority + 1; + } else { + $type->priority = 1; + } + $type->margic = $moduleinstance->id; $DB->insert_record('margic_errortypes', $type); @@ -110,24 +116,27 @@ } // Change priority. -if ($manageerrortypes && $mode == 2 && $priority && $action && $DB->record_exists('margic_errortypes', array('id' => $priority))) { +if ($manageerrortypes && $mode == 2 && $priority && $action && $DB->record_exists('margic_errortypes', ['id' => $priority])) { require_sesskey(); - $redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/margic/error_summary.php', ['id' => $id]); + + $type = $DB->get_record('margic_errortypes', ['margic' => $moduleinstance->id, 'id' => $priority]); - $type = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'id' => $priority)); + $etypes = $margic->get_margic_errortypes(); $prioritychanged = false; $oldpriority = 0; - if ($type && $action == 1 && $type->priority != 1) { // Increase priority (show more in front). + // Increase priority (show more in front). + if ($type && $action == 1 && $type->priority != $etypes[array_key_first($etypes)]->priority) { $oldpriority = $type->priority; $type->priority -= 1; $prioritychanged = true; - $typeswitched = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'priority' => $type->priority)); + $typeswitched = $DB->get_record('margic_errortypes', ['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'); @@ -137,13 +146,13 @@ } } else if ($type && $action == 2 && $type->priority != $DB->count_records('margic_errortypes', - array('margic' => $moduleinstance->id)) + 1) { // Decrease priority (move further back). + ['margic' => $moduleinstance->id]) + 1) { // Decrease priority (move further back). $oldpriority = $type->priority; $type->priority += 1; $prioritychanged = true; - $typeswitched = $DB->get_record('margic_errortypes', array('margic' => $moduleinstance->id, 'priority' => $type->priority)); + $typeswitched = $DB->get_record('margic_errortypes', ['margic' => $moduleinstance->id, 'priority' => $type->priority]); if (!$typeswitched) { // If no type with priority+1 search for types with higher priority values. $typeswitched = $DB->get_records_select('margic_errortypes', @@ -176,7 +185,7 @@ require_sesskey(); - $redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id)); + $redirecturl = new moodle_url('/mod/margic/error_summary.php', ['id' => $id]); if ($mode == 1) { // If type is template error type. $table = 'margic_errortype_templates'; @@ -184,15 +193,15 @@ $table = 'margic_errortypes'; } - if ($DB->record_exists($table, array('id' => $delete))) { + if ($DB->record_exists($table, ['id' => $delete])) { - $type = $DB->get_record($table, array('id' => $delete)); + $type = $DB->get_record($table, ['id' => $delete]); if ($mode == 2 || ($type->defaulttype == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) || ($type->defaulttype == 0 && $type->userid == $USER->id)) { - $DB->delete_records($table, array('id' => $delete)); + $DB->delete_records($table, ['id' => $delete]); redirect($redirecturl, get_string('errortypedeleted', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); @@ -203,11 +212,9 @@ } // Get the name for this margic activity. -$margicname = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$margicname = format_string($moduleinstance->name, true, ['context' => $context]); -$PAGE->set_url('/mod/margic/error_summary.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/margic/error_summary.php', ['id' => $cm->id]); $PAGE->navbar->add(get_string('errorsummary', 'mod_margic')); $PAGE->set_title(get_string('modulename', 'mod_margic').': ' . $margicname); @@ -233,7 +240,7 @@ foreach ($participants as $key => $participant) { if (has_capability('mod/margic:viewerrorsfromallparticipants', $context) || $participant->id == $USER->id) { - $participants[$key]->errors = array(); + $participants[$key]->errors = []; foreach ($errortypes as $i => $type) { $sql = "SELECT COUNT(*) @@ -242,7 +249,7 @@ WHERE e.margic = :margic AND e.userid = :userid AND a.type = :atype"; - $params = array('margic' => $moduleinstance->id, 'userid' => $participant->id, 'atype' => $i); + $params = ['margic' => $moduleinstance->id, 'userid' => $participant->id, 'atype' => $i]; $count = $DB->count_records_sql($sql, $params); $participants[$key]->errors[$i] = $count; diff --git a/errortypes.php b/errortypes.php index 8d55103..c5eb248 100644 --- a/errortypes.php +++ b/errortypes.php @@ -61,9 +61,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -71,11 +69,11 @@ require_capability('mod/margic:manageerrortypes', $context); -$redirecturl = new moodle_url('/mod/margic/error_summary.php', array('id' => $id)); +$redirecturl = new moodle_url('/mod/margic/error_summary.php', ['id' => $id]); if ($edit !== 0) { if ($mode == 1) { // If type is template error type. - $editedtype = $DB->get_record('margic_errortype_templates', array('id' => $edit)); + $editedtype = $DB->get_record('margic_errortype_templates', ['id' => $edit]); if (isset($editedtype->defaulttype) && $editedtype->defaulttype == 1 && !get_config('margic', 'defaulterrortypetemplateseditable')) { @@ -83,7 +81,7 @@ redirect($redirecturl, get_string('notallowedtodothis', 'mod_margic'), null, notification::NOTIFY_ERROR); } } else if ($mode == 2) { // If type is margic error type. - $editedtype = $DB->get_record('margic_errortypes', array('id' => $edit)); + $editedtype = $DB->get_record('margic_errortypes', ['id' => $edit]); if ($moduleinstance->id !== $editedtype->margic) { redirect($redirecturl, get_string('errortypecantbeedited', 'mod_margic'), null, notification::NOTIFY_ERROR); @@ -98,7 +96,7 @@ $editedtypeid = $edit; $editedtypename = $editedtype->name; - $editedcolor = $editedtype->color; + $editedcolor = '#' . $editedtype->color; if ($mode == 1) { $editeddefaulttype = $editedtype->defaulttype; @@ -108,18 +106,18 @@ // Instantiate form. $mform = new \mod_margic_errortypes_form(null, - array('editdefaulttype' => has_capability('mod/margic:editdefaulterrortypes', $context), 'mode' => $mode)); + ['editdefaulttype' => has_capability('mod/margic:editdefaulterrortypes', $context), 'mode' => $mode]); 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(['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)); + $mform->set_data(['id' => $id, 'mode' => $mode, 'typeid' => $editedtypeid, 'typename' => $editedtypename, + 'color' => $editedcolor, ]); } } else { - $mform->set_data(array('id' => $id, 'mode' => $mode)); + $mform->set_data(['id' => $id, 'mode' => $mode]); } if ($mform->is_cancelled()) { @@ -133,7 +131,7 @@ $errortype = new stdClass(); $errortype->timecreated = time(); $errortype->timemodified = 0; - $errortype->name = format_text($fromform->typename, 1, array('para' => false)); + $errortype->name = format_text($fromform->typename, 1, ['para' => false]); $errortype->color = $fromform->color; if (isset($fromform->standardtype) && $fromform->standardtype === 1 && @@ -147,7 +145,13 @@ } if ($mode == 2) { // If type is margic error type. - $errortype->priority = $margic->get_margic_errortypes()[array_key_last($margic->get_margic_errortypes())]->priority + 1; + $etypes = $margic->get_margic_errortypes(); + if ($etypes) { + $errortype->priority = $etypes[array_key_last($etypes)]->priority + 1; + } else { + $errortype->priority = 1; + } + $errortype->margic = $moduleinstance->id; } @@ -162,9 +166,9 @@ } else if ($fromform->typeid !== 0 && isset($fromform->typename)) { // Update existing annotation type. if ($mode == 1) { // If type is template error type. - $errortype = $DB->get_record('margic_errortype_templates', array('id' => $fromform->typeid)); + $errortype = $DB->get_record('margic_errortype_templates', ['id' => $fromform->typeid]); } else if ($mode == 2) { // If type is margic error type. - $errortype = $DB->get_record('margic_errortypes', array('id' => $fromform->typeid)); + $errortype = $DB->get_record('margic_errortypes', ['id' => $fromform->typeid]); } if ($errortype && @@ -175,7 +179,7 @@ && $errortype->userid == $USER->id))) { $errortype->timemodified = time(); - $errortype->name = format_text($fromform->typename, 1, array('para' => false)); + $errortype->name = format_text($fromform->typename, 1, ['para' => false]); $errortype->color = $fromform->color; if ($mode == 1 && has_capability('mod/margic:editdefaulterrortypes', $context)) { @@ -207,15 +211,12 @@ } // Get the name for this margic activity. -$margicname = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$margicname = format_string($moduleinstance->name, true, ['context' => $context]); -$PAGE->set_url('/mod/margic/errortypes.php', array('id' => $cm->id)); +$PAGE->set_url('/mod/margic/errortypes.php', ['id' => $cm->id]); $navtitle = ''; - if (isset($editedtypeid)) { $navtitle = get_string('editerrortype', 'mod_margic'); } else { diff --git a/errortypes_form.php b/errortypes_form.php index 8745ca1..b66b577 100644 --- a/errortypes_form.php +++ b/errortypes_form.php @@ -41,7 +41,7 @@ class mod_margic_errortypes_form extends moodleform { */ public function definition() { - global $OUTPUT; + global $OUTPUT, $CFG; $mform = $this->_form; // Don't forget the underscore! @@ -62,7 +62,12 @@ public function definition() { $mform->addHelpButton('typename', 'explanationtypename', 'mod_margic'); } - $mform->addElement('text', 'color', get_string('annotationcolor', 'mod_margic')); + MoodleQuickForm::registerElementType('colorpicker', + "$CFG->dirroot/mod/margic/classes/forms/mod_margic_colorpicker_form_element.php", + 'mod_margic_colorpicker_form_element'); + + $mform->addElement('colorpicker', 'color', get_string('explanationhexcolor', 'mod_margic')); + $mform->setType('color', PARAM_ALPHANUM); $mform->addRule('color', null, 'required', null, 'client'); $mform->addHelpButton('color', 'explanationhexcolor', 'mod_margic'); @@ -88,7 +93,7 @@ public function definition() { * @return array Array with errors */ public function validation($data, $files) { - $errors = array(); + $errors = []; if (strlen($data['color']) !== 6 || preg_match("/[^a-fA-F0-9]/", $data['color'])) { $errors['color'] = get_string('errnohexcolor', 'mod_margic'); diff --git a/grade_entry.php b/grade_entry.php index a6208a7..96d4e21 100644 --- a/grade_entry.php +++ b/grade_entry.php @@ -55,9 +55,7 @@ throw new moodle_exception(get_string('incorrectcourseid', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -65,9 +63,9 @@ require_capability('mod/margic:addentries', $context); -$PAGE->set_url('/mod/margic/grade_entry.php', array('id' => $id)); +$PAGE->set_url('/mod/margic/grade_entry.php', ['id' => $id]); -$entry = $DB->get_record('margic_entries', array('id' => $entryid, 'margic' => $cm->instance)); +$entry = $DB->get_record('margic_entries', ['id' => $entryid, 'margic' => $cm->instance]); $grades = make_grades_menu($moduleinstance->scale); // Prepare editor for files. @@ -77,6 +75,7 @@ $data->timecreated = $entry->timecreated; $data->{'feedback_' . $entry->id} = $entry->feedback; $data->{'feedback_' . $entry->id . 'format'} = $entry->formatfeedback; +$data->sendgradingmessage = $moduleinstance->sendgradingmessage; list ($editoroptions, $attachmentoptions) = helper::margic_get_editor_and_attachment_options($course, $context, $moduleinstance); @@ -88,8 +87,8 @@ $data->{'rating_' . $entry->id} = $entry->rating; // Instantiate gradingform and save submitted data if it exists. -$mform = new \mod_margic_grading_form(null, array('courseid' => $course->id, 'margic' => $moduleinstance, 'entry' => $entry, - 'grades' => $grades, 'teacherimg' => '', 'editoroptions' => $editoroptions)); +$mform = new \mod_margic_grading_form(null, ['courseid' => $course->id, 'margic' => $moduleinstance, 'entry' => $entry, + 'grades' => $grades, 'teacherimg' => '', 'editoroptions' => $editoroptions, ]); $mform->set_data($data); @@ -97,12 +96,12 @@ // In this case you process validated data. if ($fromform->entry !== $entryid) { - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('errfeedbacknotupdated', 'mod_margic'), + redirect(new moodle_url('/mod/margic/view.php', ['id' => $id]), get_string('errfeedbacknotupdated', 'mod_margic'), null, notification::NOTIFY_ERROR); } if (!$fromform->{'feedback_' . $entry->id . '_editor'}) { - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), + redirect(new moodle_url('/mod/margic/view.php', ['id' => $id]), get_string('errnofeedbackorratingdisabled', 'mod_margic'), null, notification::NOTIFY_ERROR); } @@ -112,7 +111,7 @@ $newfeedback = file_rewrite_pluginfile_urls($fromform->{'feedback_' . $entry->id}, 'pluginfile.php', $context->id, 'mod_margic', 'feedback', $entry->id); - $newfeedback = format_text($newfeedback, $fromform->{'feedback_' . $entry->id . '_editor'}['format'], array('para' => false)); + $newfeedback = format_text($newfeedback, $fromform->{'feedback_' . $entry->id . '_editor'}['format'], ['para' => false]); if (isset($fromform->{'rating_' . $entry->id})) { $newrating = $fromform->{'rating_' . $entry->id}; @@ -143,7 +142,7 @@ $entry->timemarked = $timenow; if (!$DB->update_record("margic_entries", $entry)) { - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('errfeedbacknotupdated', 'mod_margic'), + redirect(new moodle_url('/mod/margic/view.php', ['id' => $id]), get_string('errfeedbacknotupdated', 'mod_margic'), null, notification::NOTIFY_ERROR); } @@ -179,10 +178,10 @@ margic_update_grades($record, $entry->userid); // Trigger module feedback updated event. - $event = \mod_margic\event\feedback_updated::create(array( + $event = \mod_margic\event\feedback_updated::create([ 'objectid' => $entry->id, - 'context' => $context - )); + 'context' => $context, + ]); $event->trigger(); if ($fromform->sendgradingmessage) { @@ -193,7 +192,7 @@ $obj = new stdClass(); $obj->user = fullname(core_user::get_user($entry->userid)); $obj->teacher = fullname(core_user::get_user($entry->teacher)); - $obj->margic = format_string($moduleinstance->name, true); + $obj->margic = format_string($moduleinstance->name); $obj->url = $url; // Send grading message. @@ -216,8 +215,8 @@ $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. + 'coursename' => $course->fullname, 'name' => $moduleinstance->name, 'url' => $url, ]); + $content = ['*' => ['header' => $header, 'footer' => $footer]]; // Extra content for specific processor. $message->set_additional_content('email', $content); // Actually send the message. @@ -225,13 +224,16 @@ } // Redirect after updated from feedback and grades. - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('feedbackupdated', 'mod_margic'), - null, notification::NOTIFY_SUCCESS); + redirect(new moodle_url('/mod/margic/view.php', + ['id' => $id, 'focusgradingform' => $entry->id, 'annotationmode' => 1]), + get_string('feedbackupdated', 'mod_margic'), null, notification::NOTIFY_SUCCESS); } else { - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('errfeedbacknotupdated', 'mod_margic'), - null, notification::NOTIFY_ERROR); + redirect(new moodle_url('/mod/margic/view.php', + ['id' => $id, 'focusgradingform' => $entry->id, 'annotationmode' => 1]), + get_string('errfeedbacknotupdated', 'mod_margic'), null, notification::NOTIFY_ERROR); } } else { - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), get_string('errfeedbacknotupdated', 'mod_margic'), - null, notification::NOTIFY_ERROR); + redirect(new moodle_url('/mod/margic/view.php', + ['id' => $id, 'focusgradingform' => $entry->id, 'annotationmode' => 1]), + get_string('errfeedbacknotupdated', 'mod_margic'), null, notification::NOTIFY_ERROR); } diff --git a/grading_form.php b/grading_form.php index 5205eaf..8ec9575 100644 --- a/grading_form.php +++ b/grading_form.php @@ -58,11 +58,11 @@ public function definition() { $feedbacktext = $this->_customdata['entry']->feedback; - $user = $DB->get_record('user', array('id' => $this->_customdata['entry']->userid)); + $user = $DB->get_record('user', ['id' => $this->_customdata['entry']->userid]); $userfullname = fullname($user); $feedbackdisabled = false; - $attr = array(); + $attr = []; if ($this->_customdata['margic']->assessed != 0) { // Append grading area only when grading is not disabled. @@ -90,7 +90,7 @@ public function definition() { $this->_customdata['courseid'] . '">' . $gradinginfo->items[0]->grades[$this->_customdata['entry']->userid]->str_feedback . ''; - $attr = array('disabled' => 'disabled'); + $attr = ['disabled' => 'disabled']; } } @@ -101,10 +101,18 @@ public function definition() { $mform->addElement('html', '
'); - if ($this->_customdata['entry']->timemarked) { - $mform->addElement('static', 'currentuserrating', - get_string('grader', 'mod_margic'), $this->_customdata['teacherimg'] . ' - ' + if (isset($this->_customdata['hasteacher'])) { + + if ($this->_customdata['entry']->timemarked) { + $mform->addElement('static', 'currentuserrating', + get_string('grader', 'mod_margic'), $this->_customdata['teacherimg'] . ' - ' . userdate($this->_customdata['entry']->timemarked)); + + } else { + $mform->addElement('static', 'currentuserrating', + get_string('grader', 'mod_margic'), $this->_customdata['teacherimg']); + } + $mform->addElement('static', 'savedrating', get_string('savedrating', 'mod_margic'), $this->_customdata['entry']->rating); } @@ -131,7 +139,6 @@ public function definition() { $mform->setType('feedback_' . $this->_customdata['entry']->id . '_editor', PARAM_RAW); $mform->addElement('selectyesno', 'sendgradingmessage', get_string('sendgradingmessage', 'margic')); - $mform->setDefault('sendgradingmessage', 1); $this->add_action_buttons(); } diff --git a/index.php b/index.php index 14a9490..6d8d101 100644 --- a/index.php +++ b/index.php @@ -27,9 +27,7 @@ $id = required_param('id', PARAM_INT); // Course. -if (! $course = $DB->get_record('course', array( - 'id' => $id -))) { +if (! $course = $DB->get_record('course', ['id' => $id])) { throw new moodle_exception(get_string('incorrectcourseid', 'margic')); } @@ -38,9 +36,7 @@ // Header. $strmargics = get_string('modulenameplural', 'margic'); $PAGE->set_pagelayout('incourse'); -$PAGE->set_url('/mod/margic/index.php', array( - 'id' => $id -)); +$PAGE->set_url('/mod/margic/index.php', ['id' => $id]); $PAGE->navbar->add($strmargics); $PAGE->set_title($strmargics); $PAGE->set_heading($course->fullname); @@ -65,8 +61,8 @@ // Table data. $table = new html_table(); -$table->head = array(); -$table->align = array(); +$table->head = []; +$table->align = []; if ($usesections) { // Add column heading based on the course format. e.g. Week, Topic. $table->head[] = get_string('sectionname', 'format_' . $course->format); @@ -102,9 +98,7 @@ } // Link. - $margicname = format_string($margic->name, true, array( - 'context' => $context - )); + $margicname = format_string($margic->name, true, ['context' => $context]); if (! $margic->visible) { // Show dimmed if the mod is hidden. $table->data[$i][] = "coursemodule\">" . $margicname . ""; @@ -124,9 +118,7 @@ echo html_writer::table($table); // Trigger course module instance list event. -$params = array( - 'context' => context_course::instance($course->id) -); +$params = ['context' => context_course::instance($course->id)]; $event = \mod_margic\event\course_module_instance_list_viewed::create($params); $event->add_record_snapshot('course', $course); $event->trigger(); diff --git a/lang/de/margic.php b/lang/de/margic.php index 8925cf0..c1830ff 100644 --- a/lang/de/margic.php +++ b/lang/de/margic.php @@ -111,7 +111,6 @@ $string['notstarted'] = 'Sie haben noch keine Margic Einträge angelegt'; $string['noentriesfound'] = 'Keine Einträge gefunden'; $string['viewallentries'] = 'Alle Einträge ansehen'; -$string['viewallmargics'] = 'Alle Margics im Kurs anzeigen'; // Annotations. $string['annotationcreated'] = 'Erstellt am {$a}'; @@ -139,6 +138,10 @@ $string['margicclosetime_help'] = 'Wenn aktiviert können Sie ein Datum festlegen, bis zu dem Einträge im Margic anlegen oder bearbeitet werden können.'; $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}).'; +$string['defaultforsendgradingmessage'] = 'Standardwert für die Benachrichtigung bei Feedback'; +$string['defaultforsendgradingmessage_help'] = 'Hier kann der Standardwert für die Benachrichtigung bei Feedback eingestellt werden. Dieser wird im Bewertungsformular vorausgefüllt, kann dort aber bei jeder Bewertung auch manuell verändert werden.'; +$string['overwriteannotations'] = 'Annotationen überschreiben'; +$string['overwriteannotations_help'] = 'Hier kann festgelegt werden, ob Lehrende die Annotationen anderer Lehrender überschreiben sowie löschen dürfen'; // Form: edit_form. $string['addnewentry'] = 'Neuen Eintrag anlegen'; @@ -176,6 +179,7 @@ $string['errortypedeleted'] = 'Fehlertyp entfernt'; $string['deleteerrortypetemplate'] = 'Vorlage löschen'; $string['deleteerrortypetemplateconfirm'] = 'Soll diese Fehlertyp-Vorlage wirklich gelöscht werden? Dadurch wird die Vorlage für das gesamte System gelöscht und kann nicht mehr in neuen Margics als konkreter Fehlertyp ausgewählt werden. Diese Aktion kann nicht rückgängig gemacht werden!'; +$string['deleteerrortypeconfirm'] = 'Soll dieser Fehlertyp wirklich gelöscht werden? Dadurch wird er aus dem Margic entfernt und bei bestehenden Annotationen als Gelöschter Typ angezeigt. Diese Aktion kann nicht rückgängig gemacht werden!'; $string['errortypeinvalid'] = 'Fehlertyp ungültig'; $string['nameoferrortype'] = 'Name des Fehlertyps'; $string['margicerrortypes'] = 'Margic Fehlertypen'; @@ -240,6 +244,9 @@ $string['margic:viewannotations'] = 'Annotationen ansehen'; $string['margic:makeannotations'] = 'Annotationen anlegen'; $string['margic:deleteannotations'] = 'Annotationen löschen'; +$string['margic:viewotherusersentrytimes'] = 'Zeitpunkt der Erstellung fremder Einträge ansehen'; +$string['margic:viewotherusersannotationtimes'] = 'Zeitpunkt der Erstellung fremder Annotationen ansehen'; +$string['margic:viewotherusersfeedbacktimes'] = 'Zeitpunkt der Bewertung durch andere Lehrende ansehen'; // Recent activity. $string['newmargicentries'] = 'Neue Margic Einträge'; @@ -288,6 +295,8 @@ $string['editentrydates_help'] = 'Wenn aktiviert können Lehrende in jedem Margic festlegen, ob Nutzer/innen das Datum jedes neuen Eintrags bearbeiten können.'; $string['editentries'] = 'Eigene Einträge bearbeiten'; $string['editentries_help'] = 'Wenn aktiviert können Lehrende in jedem Margic festlegen, ob Nutzer/innen ihre eigenen Einträge bearbeiten können.'; +$string['sendgradingmessagedefault'] = 'Ersteller/innen von Einträgen über Bewertung informieren'; +$string['sendgradingmessagedefault_help'] = 'Legt den Standardwert für die Bewertungs-Formulare in allen Margics fest. Bestimmt, ob die Ersteller/innen von Einträgen benachrichtigt werden sollen, wenn Lehrende einen Eintrag bewerten. Kann in jedem Margic oder im Bewertungsformular selbst geändert werden.'; $string['annotationareawidth'] = 'Breite des Annotationsbereichs'; $string['annotationareawidthall'] = 'Die Breite des Annotationsbereichs in Prozent für alle Margics. Kann von Lehrenden in den einzelnen Margics überschrieben werden. Minimal 20 and maximal 80 Prozent.'; $string['editability'] = 'Bearbeitbarkeit'; diff --git a/lang/en/margic.php b/lang/en/margic.php index cf09d21..28eb178 100644 --- a/lang/en/margic.php +++ b/lang/en/margic.php @@ -111,7 +111,6 @@ $string['notstarted'] = 'You have not added any entries to this Margic yet'; $string['noentriesfound'] = 'No entries found'; $string['viewallentries'] = 'View all entries'; -$string['viewallmargics'] = 'View all margics in course'; // Annotations. $string['annotationcreated'] = 'Created at {$a}'; @@ -139,6 +138,8 @@ $string['margicclosetime_help'] = 'If activated, you can set a date until which entries can be created or edited in the Margic.'; $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}%).'; +$string['overwriteannotations'] = 'Overwrite annotations'; +$string['overwriteannotations_help'] = 'Here you can define whether teachers can overwrite and delete annotations made by other teachers.'; // Form: edit_form. $string['addnewentry'] = 'Add new entry'; @@ -161,6 +162,8 @@ $string['errfeedbacknotupdated'] = 'Feedback and grade not updated'; $string['errnograder'] = 'No grader.'; $string['errnofeedbackorratingdisabled'] = 'No feedback or rating disabled.'; +$string['defaultforsendgradingmessage'] = 'Default value for feedback notification'; +$string['defaultforsendgradingmessage_help'] = 'Here you can set the default value for the feedback notification. This is pre-filled in the rating form, but can also be manually changed there for each rating.'; // Error summary. $string['errorsummary'] = 'Error summary'; @@ -176,6 +179,7 @@ $string['errortypedeleted'] = 'Error type deleted'; $string['deleteerrortypetemplate'] = 'Delete template'; $string['deleteerrortypetemplateconfirm'] = 'Should this error type template really be deleted? This deletes the template for the entire system so that it can no longer be used as a concrete error type in new Margics. This action cannot be undone!'; +$string['deleteerrortypeconfirm'] = 'Do you really want to delete this error type? This will remove it from the Margic and display it as Deleted type for existing annotations. This action cannot be undone!'; $string['errortypeinvalid'] = 'Error type invalid'; $string['nameoferrortype'] = 'Name of error type'; $string['margicerrortypes'] = 'Margic error types'; @@ -240,6 +244,9 @@ $string['margic:viewannotations'] = 'View annotations'; $string['margic:makeannotations'] = 'Make annotations'; $string['margic:deleteannotations'] = 'Delete annotations'; +$string['margic:viewotherusersentrytimes'] = 'View time of creation for foreign entries '; +$string['margic:viewotherusersannotationtimes'] = 'View time of creation for foreign annotations '; +$string['margic:viewotherusersfeedbacktimes'] = 'View the time of the grading by other teachers '; // Recent activity. $string['newmargicentries'] = 'New Margic entries'; @@ -288,6 +295,8 @@ $string['editentrydates_help'] = 'If enabled, teachers can configure in each Margic whether users can edit their own entries.'; $string['editentries'] = 'Edit own entries'; $string['editentries_help'] = 'If enabled, teachers can configure in each Margic whether users can edit the date of each new entry.'; +$string['sendgradingmessagedefault'] = 'Notify entry creators about feedback'; +$string['sendgradingmessagedefault_help'] = 'Set the default value for the feedback forms in all Margics. Defines if entry creators should be notified if the teacher grades an entry. Can be changed in each Margic or in the feedback form itself.'; $string['annotationareawidth'] = 'Width of the annotation area'; $string['annotationareawidthall'] = 'The width of the annotation area in percent for all margics. Can be overridden by teachers in the individual margics. Minimum 20 and maximum 80 percent.'; $string['editability'] = 'Editability'; diff --git a/lib.php b/lib.php index a1864ef..05b88d8 100644 --- a/lib.php +++ b/lib.php @@ -51,7 +51,7 @@ function margic_add_instance($margic) { helper::margic_update_calendar($margic, $margic->coursemodule); // Add expected completion date. - if (! empty($margic->completionexpected)) { + if (!empty($margic->completionexpected)) { \core_completion\api::update_completion_date_event($margic->coursemodule, 'margic', $margic->id, $margic->completionexpected); } @@ -63,7 +63,7 @@ function margic_add_instance($margic) { $priority = 1; foreach ($margic->errortypes as $id => $checked) { if ($checked) { - $type = $DB->get_record('margic_errortype_templates', array('id' => $id)); + $type = $DB->get_record('margic_errortype_templates', ['id' => $id]); $type->margic = $margic->id; $type->priority = $priority; @@ -108,7 +108,7 @@ function margic_update_instance($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. - $oldmargic = $DB->get_record('margic', array('id' => $margic->id)); + $oldmargic = $DB->get_record('margic', ['id' => $margic->id]); $updategrades = false; @@ -132,7 +132,7 @@ function margic_update_instance($margic) { helper::margic_update_calendar($margic, $margic->coursemodule); // Update completion date. - $completionexpected = (! empty($margic->completionexpected)) ? $margic->completionexpected : null; + $completionexpected = (!empty($margic->completionexpected)) ? $margic->completionexpected : null; \core_completion\api::update_completion_date_event($margic->coursemodule, 'margic', $margic->id, $completionexpected); // Update grade. @@ -153,13 +153,13 @@ function margic_update_instance($margic) { function margic_delete_instance($id) { global $DB; - if (!$margic = $DB->get_record("margic", array("id" => $id))) { + if (!$margic = $DB->get_record("margic", ["id" => $id])) { return false; } if (!$cm = get_coursemodule_from_instance('margic', $margic->id)) { return false; } - if (!$course = $DB->get_record('course', array('id' => $cm->course))) { + if (!$course = $DB->get_record('course', ['id' => $cm->course])) { return false; } @@ -176,16 +176,16 @@ function margic_delete_instance($id) { margic_grade_item_delete($margic); // Delete entries. - $DB->delete_records("margic_entries", array("margic" => $margic->id)); + $DB->delete_records("margic_entries", ["margic" => $margic->id]); // Delete annotations. - $DB->delete_records("margic_annotations", array("margic" => $margic->id)); + $DB->delete_records("margic_annotations", ["margic" => $margic->id]); // Delete error types for margic. - $DB->delete_records("margic_errortypes", array("margic" => $margic->id)); + $DB->delete_records("margic_errortypes", ["margic" => $margic->id]); // Delete margic, else return false. - if (!$DB->delete_records("margic", array("id" => $margic->id))) { + if (!$DB->delete_records("margic", ["id" => $margic->id])) { return false; } @@ -248,7 +248,7 @@ function margic_supports($feature) { function margic_user_outline($course, $user, $mod, $margic) { global $DB; - if ($count = $DB->count_records("margic_entries", array("userid" => $user->id, "margic" => $margic->id))) { + if ($count = $DB->count_records("margic_entries", ["userid" => $user->id, "margic" => $margic->id])) { $result = new stdClass(); $result->info = $count . ' ' . get_string("entries"); return $result; @@ -269,18 +269,18 @@ function margic_user_outline($course, $user, $mod, $margic) { function margic_print_recent_activity($course, $viewfullnames, $timestart) { global $CFG, $USER, $DB, $OUTPUT; - $params = array( + $params = [ $timestart, $course->id, - 'margic' - ); + 'margic', + ]; // Moodle branch check. if ($CFG->branch < 311) { $namefields = user_picture::fields('u', null, 'userid'); } else { $userfieldsapi = \core_user\fields::for_userpic(); - $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; + $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; } $sql = "SELECT e.id, e.timecreated, cm.id AS cmid, $namefields @@ -297,7 +297,7 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { $modinfo = get_fast_modinfo($course); - $show = array(); + $show = []; foreach ($newentries as $entry) { if (! array_key_exists($entry->cmid, $modinfo->get_cms())) { @@ -352,6 +352,7 @@ function margic_print_recent_activity($course, $viewfullnames, $timestart) { echo $OUTPUT->heading(get_string('newmargicentries', 'margic') . ':', 6); foreach ($show as $entry) { + $cm = $modinfo->get_cm($entry->cmid); $context = context_module::instance($entry->cmid); $link = $CFG->wwwroot . '/mod/margic/view.php?id=' . $cm->id; @@ -382,13 +383,13 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour if ($COURSE->id == $courseid) { $course = $COURSE; } else { - $course = $DB->get_record('course', array('id' => $courseid)); + $course = $DB->get_record('course', ['id' => $courseid]); } $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cmid); - $params = array(); + $params = []; if ($userid) { $userselect = 'AND u.id = :userid'; $params['userid'] = $userid; @@ -437,7 +438,7 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); $teacher = has_capability('mod/margic:manageentries', $cmcontext); - $show = array(); + $show = []; foreach ($entries as $entry) { if ($entry->userid == $USER->id) { $show[] = $entry; @@ -477,14 +478,15 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour if ($grader) { require_once($CFG->libdir.'/gradelib.php'); - $userids = array(); + $userids = []; foreach ($show as $id => $entry) { $userids[] = $entry->userid; } $grades = grade_get_grades($courseid, 'mod', 'margic', $cm->instance, $userids); } - $aname = format_string($cm->name, true); + $aname = format_string($cm->name); + foreach ($show as $entry) { $activity = new stdClass(); @@ -494,7 +496,7 @@ function margic_get_recent_mod_activity(&$activities, &$index, $timestart, $cour $activity->sectionnum = $cm->sectionnum; $activity->timestamp = $entry->timecreated; $activity->user = new stdClass(); - if ($grader) { + if ($grader && $grades->items && isset($entry->userid)) { $activity->grade = $grades->items[0]->grades[$entry->userid]->str_long_grade; } @@ -581,7 +583,7 @@ function margic_reset_course_form_definition(&$mform) { * @return array */ function margic_reset_course_form_defaults($course) { - return array('reset_margic_all' => 1, 'reset_margic_errortypes' => 1); + return ['reset_margic_all' => 1, 'reset_margic_errortypes' => 1]; } /** @@ -598,16 +600,16 @@ function margic_reset_userdata($data) { require_once($CFG->dirroot . '/rating/lib.php'); $modulename = get_string('modulenameplural', 'margic'); - $status = array(); + $status = []; // Get margics in course that should be resetted. $sql = "SELECT m.id FROM {margic} m WHERE m.course = ?"; - $params = array( - $data->courseid - ); + $params = [ + $data->courseid, + ]; $margics = $DB->get_records_sql($sql, $params); @@ -648,18 +650,18 @@ function margic_reset_userdata($data) { // Delete all entries. $DB->delete_records_select('margic_entries', "margic IN ($sql)", $params); - $status[] = array( + $status[] = [ 'component' => $modulename, 'item' => get_string('alluserdatadeleted', 'margic'), - 'error' => false - ); + 'error' => false, + ]; } // Delete errortypes. if (!empty($data->reset_margic_errortypes) ) { $DB->delete_records_select('margic_errortypes', "margic IN ($sql)", $params); - $status[] = array('component' => $modulename, 'item' => get_string('errortypesdeleted', 'margic'), 'error' => false); + $status[] = ['component' => $modulename, 'item' => get_string('errortypesdeleted', 'margic'), 'error' => false]; } @@ -667,9 +669,9 @@ function margic_reset_userdata($data) { if ($data->timeshift) { // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. // See MDL-9367. - shift_course_mod_dates('margic', array('assesstimestart', 'assesstimefinish', 'timeopen', 'timeclose'), + shift_course_mod_dates('margic', ['assesstimestart', 'assesstimefinish', 'timeopen', 'timeclose'], $data->timeshift, $data->courseid); - $status[] = array('component' => $modulename, 'item' => get_string('datechanged'), 'error' => false); + $status[] = ['component' => $modulename, 'item' => get_string('datechanged'), 'error' => false]; } return $status; @@ -683,7 +685,7 @@ function margic_reset_userdata($data) { function margic_reset_gradebook($courseid) { global $DB; - $params = array($courseid); + $params = [$courseid]; $sql = "SELECT ma.*, cm.idnumber as cmidnumber, ma.course as courseid FROM {margic} ma, {course_modules} cm, {modules} m @@ -764,10 +766,10 @@ function margic_grade_item_update($margic, $grades = null) { global $CFG; require_once($CFG->libdir . '/gradelib.php'); - $params = array( + $params = [ 'itemname' => $margic->name, - 'idnumber' => $margic->cmidnumber - ); + 'idnumber' => $margic->cmidnumber, + ]; if (! $margic->assessed || $margic->scale == 0) { $params['gradetype'] = GRADE_TYPE_NONE; @@ -799,9 +801,9 @@ function margic_grade_item_delete($margic) { require_once($CFG->libdir . '/gradelib.php'); - return grade_update('mod/margic', $margic->course, 'mod', 'margic', $margic->id, 0, null, array( - 'deleted' => 1 - )); + return grade_update('mod/margic', $margic->course, 'mod', 'margic', $margic->id, 0, null, [ + 'deleted' => 1, + ]); } /** @@ -834,7 +836,7 @@ function margic_scale_used_anywhere($scaleid) { * @param array $options Additional options affecting the file serving. * @return bool False if file not found, does not return if found - just send the file. */ -function margic_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { +function margic_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) { global $DB, $USER; if ($context->contextlevel != CONTEXT_MODULE) { @@ -849,9 +851,7 @@ function margic_pluginfile($course, $cm, $context, $filearea, $args, $forcedownl // Args[0] should be the entry id. $entryid = intval(array_shift($args)); - $entry = $DB->get_record('margic_entries', array( - 'id' => $entryid - ), 'id, userid', MUST_EXIST); + $entry = $DB->get_record('margic_entries', ['id' => $entryid], 'id, userid', MUST_EXIST); $canmanage = has_capability('mod/margic:manageentries', $context); if (! $canmanage && ! has_capability('mod/margic:addentries', $context)) { @@ -876,27 +876,3 @@ function margic_pluginfile($course, $cm, $context, $filearea, $args, $forcedownl // Finally send the file. send_stored_file($file, null, 0, $forcedownload, $options); } - -/** - * Extends the global navigation tree by adding mod_margic nodes if there is a relevant content. - * - * This can be called by an AJAX request so do not rely on $PAGE as it might not be set up properly. - * - * @param navigation_node $margicnode An object representing the navigation tree node. - * @param stdClass $course Course object - * @param context_course $coursecontext Course context - */ -function margic_extend_navigation_course($margicnode, $course, $coursecontext) { - $modinfo = get_fast_modinfo($course); // Get mod_fast_modinfo from $course. - $index = 1; // Set index. - foreach ($modinfo->get_cms() as $cmid => $cm) { // Search existing course modules for this course. - if ($index == 1 && $cm->modname == "margic" && $cm->uservisible && $cm->available) { - $url = new moodle_url("/mod/" . $cm->modname . "/index.php", - array("id" => $course->id)); // Set url for the link in the navigation node. - $node = navigation_node::create(get_string('viewallmargics', 'margic'), $url, - navigation_node::TYPE_CUSTOM, null , null , null); - $margicnode->add_node($node); - $index++; - } - } -} diff --git a/locallib.php b/locallib.php index 4659f7e..580947b 100644 --- a/locallib.php +++ b/locallib.php @@ -62,16 +62,16 @@ class margic { private $page; /** @var array Array with all accessible entries of the margic instance */ - private $entries = array(); + private $entries = []; /** @var array Array with all annotations to entries of the margic instance */ - private $annotations = array(); + private $annotations = []; /** @var array Array with all types of annotations */ - private $errortypes = array(); + private $errortypes = []; /** @var array Array of error messages encountered during the execution of margic related operations. */ - private $errors = array(); + private $errors = []; /** * Constructor for the base margic class. @@ -118,21 +118,27 @@ function sortannotation($a, $b) { $this->cm = cm_info::create($cm); - $this->instance = $DB->get_record('margic', array('id' => $this->cm->instance)); + $this->instance = $DB->get_record('margic', ['id' => $this->cm->instance]); $this->modulename = get_string('modulename', 'mod_margic'); - $this->annotations = $DB->get_records('margic_annotations', array('margic' => $this->get_course_module()->instance)); + $this->annotations = $DB->get_records('margic_annotations', ['margic' => $this->get_course_module()->instance]); $select = "margic = " . $this->instance->id; $this->errortypes = (array) $DB->get_records_select('margic_errortypes', $select, null, 'priority ASC'); foreach ($this->annotations as $key => $annotation) { + // Check if annotation creation time should be shown. + if (!has_capability('mod/margic:viewotherusersannotationtimes', $context) && $annotation->userid != $USER->id) { + $this->annotations[$key]->timecreated = false; + $this->annotations[$key]->timemodified = false; + } + if (!array_key_exists($annotation->type, $this->errortypes) && - $DB->record_exists('margic_errortypes', array('id' => $annotation->type))) { + $DB->record_exists('margic_errortypes', ['id' => $annotation->type])) { - $this->errortypes[$annotation->type] = $DB->get_record('margic_errortypes', array('id' => $annotation->type)); + $this->errortypes[$annotation->type] = $DB->get_record('margic_errortypes', ['id' => $annotation->type]); } if (isset($this->errortypes[$annotation->type])) { @@ -244,16 +250,16 @@ function sortannotation($a, $b) { if ($this->mode == 'allentries') { if ($userid && $userid != 0) { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $userid, - 'baseentry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', ['margic' => $this->instance->id, 'userid' => $userid, + 'baseentry' => null, ], $sortoptions); } else { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, - 'baseentry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', ['margic' => $this->instance->id, + 'baseentry' => null, ], $sortoptions); } } else if ($this->mode == 'ownentries') { - $this->entries = $DB->get_records('margic_entries', array('margic' => $this->instance->id, 'userid' => $USER->id, - 'baseentry' => null), $sortoptions); + $this->entries = $DB->get_records('margic_entries', ['margic' => $this->instance->id, 'userid' => $USER->id, + 'baseentry' => null, ], $sortoptions); } } @@ -361,7 +367,7 @@ public function get_margic_errortypes() { * @return array action */ public function get_errortypes_for_form() { - $types = array(); + $types = []; $strmanager = get_string_manager(); foreach ($this->errortypes as $key => $type) { if ($strmanager->string_exists($type->name, 'mod_margic')) { @@ -428,7 +434,7 @@ public function get_pagebar() { unset($groupedentries[0]); if (isset($groupedentries[2])) { - $pagebar = array(); + $pagebar = []; foreach ($groupedentries as $pagenr => $page) { $obj = new stdClass(); if ($pagenr == $this->page) { @@ -458,9 +464,9 @@ public function get_pagebar() { */ public function get_pagecountoptions() { - $pagecountoptions = array(2, 3, 4, 5, 6, 7, 8, 9, 10, + $pagecountoptions = [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 100, 200, 300, 400, 500, - 1000); + 1000, ]; foreach ($pagecountoptions as $key => $number) { $obj = new stdClass(); @@ -510,15 +516,35 @@ public function prepare_entry($entry, $strmanager, $currentgroups, $allowedusers global $DB, $USER, $CFG, $OUTPUT; - $entry->user = $DB->get_record('user', array('id' => $entry->userid)); + $entry->user = $DB->get_record('user', ['id' => $entry->userid]); + + // Check if entry creation time should be shown. + if (!has_capability('mod/margic:viewotherusersentrytimes', $this->context) && $entry->userid != $USER->id) { + $entry->timecreated = false; + $entry->timemodified = false; + } + + // Check if feedback time should be shown. + if (!has_capability('mod/margic:viewotherusersfeedbacktimes', $this->context) && $entry->teacher != $USER->id) { + $entry->timemarked = false; + } if (!$currentgroups || ($allowedusers && in_array($entry->user, $allowedusers))) { // Get child entries for entry. $entry->childentries = $DB->get_records('margic_entries', - array('margic' => $this->instance->id, 'baseentry' => $entry->id), 'timecreated DESC'); + ['margic' => $this->instance->id, 'baseentry' => $entry->id], 'timecreated DESC'); $revisionnr = count($entry->childentries); foreach ($entry->childentries as $ci => $childentry) { + + // Check if child entry creation time should be shown. + if (!has_capability('mod/margic:viewotherusersentrytimes', $this->context) && + $childentry->userid != $USER->id) { + + $entry->childentries[$ci]->timecreated = false; + $entry->childentries[$ci]->timemodified = false; + } + $entry->childentries[$ci] = $this->prepare_entry_annotations($childentry, $strmanager, $annotationmode, $readonly); $entry->childentries[$ci]->stats = entrystats::get_entry_stats($childentry->text, $childentry->timecreated); $entry->childentries[$ci]->revision = $revisionnr; @@ -575,11 +601,11 @@ public function prepare_entry($entry, $strmanager, $currentgroups, $allowedusers require_once($CFG->dirroot . '/mod/margic/classes/local/helper.php'); $entry->user->userpicture = $OUTPUT->user_picture($entry->user, - array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25)); + ['courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 25]); // Add feedback area to entry. $entry->gradingform = helper::margic_return_feedback_area_for_entry($this->cm->id, $this->context, - $this->course, $this->instance, $entry, $grades, $canmanageentries); + $this->course, $this->instance, $entry, $grades, $canmanageentries, $this->instance->sendgradingmessage); $entry = $this->prepare_entry_annotations($entry, $strmanager, $annotationmode, $readonly); @@ -603,11 +629,17 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode // Get annotations for entry. $entry->annotations = array_values($DB->get_records('margic_annotations', - array('margic' => $this->cm->instance, 'entry' => $entry->id))); + ['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. + // Check if annotation creation time should be shown. + if (!has_capability('mod/margic:viewotherusersannotationtimes', $this->context) && $annotation->userid != $USER->id) { + $entry->annotations[$key]->timecreated = false; + $entry->annotations[$key]->timemodified = false; + } + + if (!$DB->record_exists('margic_errortypes', ['id' => $annotation->type])) { // If annotation type does not exist. $entry->annotations[$key]->color = 'FFFF00'; $entry->annotations[$key]->type = get_string('deletederrortype', 'mod_margic'); } else { @@ -620,7 +652,10 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode } } - if (has_capability('mod/margic:makeannotations', $this->context) && $annotation->userid == $USER->id && !$readonly) { + if (has_capability('mod/margic:makeannotations', $this->context) && + ($this->instance->overwriteannotations || $annotation->userid == $USER->id) && + !$readonly) { + $entry->annotations[$key]->canbeedited = true; } else { $entry->annotations[$key]->canbeedited = false; @@ -628,9 +663,9 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode if ($annotationmode) { // Add annotater images to annotations. - $annotater = $DB->get_record('user', array('id' => $annotation->userid)); + $annotater = $DB->get_record('user', ['id' => $annotation->userid]); $annotaterimage = $OUTPUT->user_picture($annotater, - array('courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20)); + ['courseid' => $this->course->id, 'link' => true, 'includefullname' => true, 'size' => 20]); $entry->annotations[$key]->userpicturestr = $annotaterimage; } else { @@ -650,10 +685,10 @@ private function prepare_entry_annotations($entry, $strmanager, $annotationmode // Add annotation form. if (!$readonly) { require_once($CFG->dirroot . '/mod/margic/annotation_form.php'); - $mform = new mod_margic_annotation_form(new moodle_url('/mod/margic/annotations.php', array('id' => $this->cm->id)), - array('types' => $this->get_errortypes_for_form())); + $mform = new mod_margic_annotation_form(new moodle_url('/mod/margic/annotations.php', ['id' => $this->cm->id]), + ['types' => $this->get_errortypes_for_form()]); // Set default data. - $mform->set_data(array('id' => $this->cm->id, 'entry' => $entry->id)); + $mform->set_data(['id' => $this->cm->id, 'entry' => $entry->id]); $entry->annotationform = $mform->render(); } diff --git a/mod_form.php b/mod_form.php index b041a38..b165657 100644 --- a/mod_form.php +++ b/mod_form.php @@ -47,9 +47,7 @@ public function definition() { $mform->addElement('header', 'general', get_string('general', 'form')); - $mform->addElement('text', 'name', get_string('margicname', 'margic'), array( - 'size' => '64' - )); + $mform->addElement('text', 'name', get_string('margicname', 'margic'), ['size' => '64']); $mform->setType('name', PARAM_TEXT); $mform->addRule('name', null, 'required', null, 'client'); @@ -85,7 +83,7 @@ public function definition() { $name .= '' . $type->name . ''; } - $mform->addElement('advcheckbox', 'errortypes[' . $id . ']', $name, ' ', array('group' => 1), array(0, 1)); + $mform->addElement('advcheckbox', 'errortypes[' . $id . ']', $name, ' ', ['group' => 1], [0, 1]); } } @@ -93,14 +91,10 @@ public function definition() { // Add the header for availability. $mform->addElement('header', 'availibilityhdr', get_string('availability')); - $mform->addElement('date_time_selector', 'timeopen', get_string('margicopentime', 'margic'), array( - 'optional' => true - )); + $mform->addElement('date_time_selector', 'timeopen', get_string('margicopentime', 'margic'), ['optional' => true]); $mform->addHelpButton('timeopen', 'margicopentime', 'margic'); - $mform->addElement('date_time_selector', 'timeclose', get_string('margicclosetime', 'margic'), array( - 'optional' => true - )); + $mform->addElement('date_time_selector', 'timeclose', get_string('margicclosetime', 'margic'), ['optional' => true]); $mform->addHelpButton('timeclose', 'margicclosetime', 'margic'); // Edit all setting if user can edit its own entries. @@ -117,6 +111,15 @@ public function definition() { $mform->setDefault('editentrydates', 0); } + // Set if entry creators should be notified about feedback for their entries by default. + $mform->addElement('selectyesno', 'sendgradingmessage', get_string('defaultforsendgradingmessage', 'margic')); + $mform->addHelpButton('sendgradingmessage', 'defaultforsendgradingmessage', 'margic'); + $mform->setDefault('sendgradingmessage', get_config('margic', 'sendgradingmessage')); + + // Set if teachers can overwrite the annotations made by other teachers. + $mform->addElement('selectyesno', 'overwriteannotations', get_string('overwriteannotations', 'margic')); + $mform->addHelpButton('overwriteannotations', 'overwriteannotations', 'margic'); + // Add the header for appearance. $mform->addElement('header', 'appearancehdr', get_string('appearance')); @@ -127,6 +130,8 @@ public function definition() { if (!isset($update) || $update == 0) { // If not updating existing instance set default to config value. $mform->setDefault('annotationareawidth', get_config('margic', 'annotationareawidth')); + + $mform->setDefault('overwriteannotations', 0); } // Add the rest of the common settings. @@ -149,8 +154,8 @@ public function validation($data, $files) { $maxwidth = 80; if (!$data['annotationareawidth'] || $data['annotationareawidth'] < $minwidth || $data['annotationareawidth'] > $maxwidth) { - $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'margic', array('minwidth' => $minwidth, - 'maxwidth' => $maxwidth)); + $errors['annotationareawidth'] = get_string('errannotationareawidthinvalid', 'margic', ['minwidth' => $minwidth, + 'maxwidth' => $maxwidth, ]); } return $errors; diff --git a/settings.php b/settings.php index fe87894..a82822f 100644 --- a/settings.php +++ b/settings.php @@ -32,26 +32,22 @@ // If default error type templates can be edited by admins or user with capability editdefaulterrortypes. $settings->add(new admin_setting_configselect('margic/defaulterrortypetemplateseditable', get_string('defaulterrortypetemplateseditable', 'margic'), - get_string('defaulterrortypetemplateseditable_help', 'margic'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); + get_string('defaulterrortypetemplateseditable_help', 'margic'), 1, ['0' => get_string('no'), '1' => get_string('yes')])); // Edit all own entries. $settings->add(new admin_setting_configselect('margic/editentries', get_string('editentries', 'margic'), - get_string('editentries_help', 'margic'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); + get_string('editentries_help', 'margic'), 1, ['0' => get_string('no'), '1' => get_string('yes')])); // Change the date of any new entry. $settings->add(new admin_setting_configselect('margic/editentrydates', get_string('editentrydates', 'margic'), - get_string('editentrydates_help', 'margic'), 1, array( - '0' => get_string('no'), - '1' => get_string('yes') - ))); + get_string('editentrydates_help', 'margic'), 1, ['0' => get_string('no'), '1' => get_string('yes')])); + + // Set if entry creators should be notified about feedback for their entries by default. + $settings->add(new admin_setting_configselect('margic/sendgradingmessage', + get_string('sendgradingmessagedefault', 'margic'), + get_string('sendgradingmessagedefault_help', 'margic'), 1, ['0' => get_string('no'), '1' => get_string('yes')])); // Appearance settings. $settings->add(new admin_setting_heading('margic/appearance', get_string('appearance'), '')); diff --git a/templates/margic_childentry.mustache b/templates/margic_childentry.mustache index e60ae70..9d803e1 100644 --- a/templates/margic_childentry.mustache +++ b/templates/margic_childentry.mustache @@ -37,8 +37,10 @@ {{{userpicture}}} {{/canmanageentries}} {{/userpicture}}{{/user}} - {{#str}}at, mod_margic {{/str}} - {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{#timecreated}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{/timecreated}} {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} {{#newestentry}}{{/newestentry}} @@ -76,8 +78,10 @@ {{{userpicturestr}}}
- {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timecreated}} + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{/timecreated}}
diff --git a/templates/margic_entry.mustache b/templates/margic_entry.mustache index cc1b599..c601908 100644 --- a/templates/margic_entry.mustache +++ b/templates/margic_entry.mustache @@ -66,8 +66,10 @@ {{{userpicture}}} {{/canmanageentries}} {{/userpicture}}{{/user}} - {{#str}}at, mod_margic {{/str}} - {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{#timecreated}} + {{#str}}at, mod_margic {{/str}} + {{#userdate}}{{timecreated}} ,{{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} + {{/timecreated}} {{^readonly}} {{#canmanageentries}}{{^singleuser}}{{/singleuser}}{{/canmanageentries}} {{^edittimehasended}}{{#caneditentries}}{{#entrycanbeedited}}{{/entrycanbeedited}}{{/caneditentries}}{{/edittimehasended}} @@ -83,9 +85,12 @@ {{#str}}details, mod_margic{{/str}}: {{#str}}numwordsraw, mod_margic, { "wordscount": {{words}}, "charscount": {{chars}}, "spacescount": {{spaces}} } {{/str}}
{{/stats}} - {{#str}}timecreated, mod_margic{{/str}}: {{#userdate}}{{timecreated}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}} - {{#stats}}{{#datediff}}({{#str}}created, mod_margic, {"years": {{datediff.y}}, "month": {{datediff.m}}, "days": {{datediff.d}}, "hours": {{datediff.h}} } {{/str}}){{/datediff}}{{/stats}} -
+ {{#timecreated}} + + {{#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}} +
+ {{/timecreated}} {{#timemodified}}
{{#str}}lastedited, mod_margic {{/str}}: {{#userdate}}{{timemodified}}, {{#str}} strftimedaydatetime, core_langconfig {{/str}}{{/userdate}}{{/timemodified}}
@@ -105,8 +110,10 @@ {{{userpicturestr}}}
- {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} - {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timecreated}} + {{^timemodified}}{{#userdate}}{{timecreated}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{#timemodified}}{{#userdate}}{{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}}{{/userdate}} {{/timemodified}} + {{/timecreated}}
diff --git a/templates/margic_error_summary.mustache b/templates/margic_error_summary.mustache index 8fb7291..4d2ba9c 100644 --- a/templates/margic_error_summary.mustache +++ b/templates/margic_error_summary.mustache @@ -46,7 +46,7 @@
{{#canbeedited}} - + {{/canbeedited}} diff --git a/templates/margic_view.mustache b/templates/margic_view.mustache index c683fc4..6b6210c 100644 --- a/templates/margic_view.mustache +++ b/templates/margic_view.mustache @@ -27,18 +27,18 @@
@@ -64,14 +64,14 @@
{{^edittimehasended}}{{^edittimenotstarted}} {{#str}}startnewentry, mod_margic{{/str}} {{/edittimenotstarted}}{{/edittimehasended}} {{#entries.0}} - {{#canmanageentries}}{{/canmanageentries}} + {{#canmanageentries}}{{/canmanageentries}} {{#canmanageentries}}{{#singleuser}} {{#str}}viewallentries, mod_margic{{/str}} {{/singleuser}}{{/canmanageentries}} {{^annotationmode}} {{#canmakeannotations}} {{#str}}annotations, mod_margic{{/str}} {{/canmakeannotations}} {{^canmakeannotations}} {{#str}}viewannotations, mod_margic{{/str}} {{/canmakeannotations}} {{/annotationmode}} {{#annotationmode}} - {{#str}}hideannotations, mod_margic{{/str}} + {{#str}}hideannotations, mod_margic{{/str}} {{/annotationmode}} {{/entries.0}} {{#str}}errorsummary, mod_margic{{/str}} diff --git a/version.php b/version.php index 672a2b3..31218fa 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_margic'; -$plugin->release = '1.2.9'; // User-friendly version number. -$plugin->version = 2023050400; // The current module version (Date: YYYYMMDDXX). +$plugin->release = '1.3.1'; // User-friendly version number. +$plugin->version = 2023102000; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061507; // Requires Moodle 3.9. $plugin->maturity = MATURITY_STABLE; diff --git a/view.php b/view.php index 496114b..ead3e34 100644 --- a/view.php +++ b/view.php @@ -54,6 +54,9 @@ // Param with id of annotation that should be focused. $focusannotation = optional_param('focusannotation', 0, PARAM_INT); // ID of annotation. +// Param with id of grading form that should be focused. +$focusgradingform = optional_param('focusgradingform', 0, PARAM_INT); // ID of grading form. + $margic = margic::get_margic_instance($id, $m, $userid, $action, $pagecount, $page); $moduleinstance = $margic->get_module_instance(); @@ -73,9 +76,7 @@ throw new moodle_exception(get_string('incorrectmodule', 'margic')); } -if (! $coursesections = $DB->get_record("course_sections", array( - "id" => $cm->section -))) { +if (! $coursesections = $DB->get_record("course_sections", ["id" => $cm->section])) { throw new moodle_exception(get_string('incorrectmodule', 'margic')); } @@ -90,7 +91,7 @@ if ($pagecount) { // Redirect if pagecount is updated. - redirect(new moodle_url('/mod/margic/view.php', array('id' => $id)), null, null, null); + redirect(new moodle_url('/mod/margic/view.php', ['id' => $id]), null, null, null); } // Toolbar action handler for download. @@ -101,49 +102,37 @@ 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 = \mod_margic\event\download_margic_entries::create(['objectid' => $id, 'context' => $context]); $event->trigger(); } // Trigger course_module_viewed event. -$event = \mod_margic\event\course_module_viewed::create(array( - 'objectid' => $id, - 'context' => $context -)); +$event = \mod_margic\event\course_module_viewed::create(['objectid' => $id, 'context' => $context]); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('margic', $moduleinstance); $event->trigger(); // Get the name for this margic activity. -$margicname = format_string($moduleinstance->name, true, array( - 'context' => $context -)); +$margicname = format_string($moduleinstance->name, true, ['context' => $context]); $canmakeannotations = has_capability('mod/margic:makeannotations', $context); // Add javascript and navbar element if annotationmode is activated and user has capability. if ($annotationmode === 1 && has_capability('mod/margic:viewannotations', $context)) { - $PAGE->set_url('/mod/margic/view.php', array( - 'id' => $cm->id, - 'annotationmode' => 1, - )); + $PAGE->set_url('/mod/margic/view.php', ['id' => $cm->id, 'annotationmode' => 1]); - $PAGE->navbar->add(get_string("viewentries", "margic"), new moodle_url('/mod/margic/view.php', array('id' => $cm->id))); + $PAGE->navbar->add(get_string("viewentries", "margic"), new moodle_url('/mod/margic/view.php', ['id' => $cm->id])); $PAGE->navbar->add(get_string('viewannotations', 'mod_margic')); $PAGE->requires->js_call_amd('mod_margic/annotations', 'init', - array( 'cmid' => $cm->id, 'canmakeannotations' => $canmakeannotations, 'myuserid' => $USER->id, - 'focusannotation' => $focusannotation)); + ['cmid' => $cm->id, 'canmakeannotations' => $canmakeannotations, 'myuserid' => $USER->id, + 'focusannotation' => $focusannotation, 'focusgradingform' => $focusgradingform, + 'overwriteannotations' => $moduleinstance->overwriteannotations, ]); } else { // Header. - $PAGE->set_url('/mod/margic/view.php', array( - 'id' => $cm->id - )); + $PAGE->set_url('/mod/margic/view.php', ['id' => $cm->id]); $PAGE->navbar->add(get_string("viewentries", "margic")); }