From be1ac30c086f0c847c5eb52e83ffdbbcabb3d64f Mon Sep 17 00:00:00 2001 From: Angel Fernando Quiroz Campos Date: Fri, 22 Oct 2021 18:15:59 -0500 Subject: [PATCH] Exercise: Allow change size/color in annotation - refs BT#18831 --- main/exercise/annotation_user.php | 13 +- main/inc/lib/exercise.lib.php | 51 ++- .../javascript/annotation/js/annotation.js | 360 ++++++++++++------ 3 files changed, 273 insertions(+), 151 deletions(-) diff --git a/main/exercise/annotation_user.php b/main/exercise/annotation_user.php index beef82a1fb..1568aabc67 100644 --- a/main/exercise/annotation_user.php +++ b/main/exercise/annotation_user.php @@ -40,20 +40,27 @@ if (!empty($questionAttempt['answer'])) { $answers = explode('|', $questionAttempt['answer']); foreach ($answers as $answer) { $parts = explode(')(', $answer); - $type = array_shift($parts); + $typeProperties = array_shift($parts); + $properties = explode(';', $typeProperties); - switch ($type) { + switch ($properties[0]) { case 'P': $points = []; foreach ($parts as $partPoint) { $points[] = Geometry::decodePoint($partPoint); } - $data['answers']['paths'][] = $points; + $data['answers']['paths'][] = [ + 'color' => $properties[1] ?? null, + 'points' => $points, + ]; break; case 'T': $text = [ 'text' => array_shift($parts), + 'color' => $properties[1] ?? null, + 'fontSize' => $properties[2] ?? null, ]; + $data['answers']['texts'][] = $text + Geometry::decodePoint($parts[0]); break; } diff --git a/main/inc/lib/exercise.lib.php b/main/inc/lib/exercise.lib.php index 41c564656c..a09e0dd94f 100644 --- a/main/inc/lib/exercise.lib.php +++ b/main/inc/lib/exercise.lib.php @@ -1704,34 +1704,31 @@ HOTSPOT;
-
-
-
-
- - -
-
- -
+
+
+
+ + +
+
+
-
    diff --git a/main/inc/lib/javascript/annotation/js/annotation.js b/main/inc/lib/javascript/annotation/js/annotation.js index fda5f39fa2..ce46bf504f 100644 --- a/main/inc/lib/javascript/annotation/js/annotation.js +++ b/main/inc/lib/javascript/annotation/js/annotation.js @@ -22,22 +22,28 @@ var SvgElementModel = function (attributes) { this.attributes = attributes; this.id = 0; - this.name = ""; + this.questionId = 0; - this.changeEvent = null; + this.changeEvents = []; + this.destroyEvents = []; }; SvgElementModel.prototype.set = function (key, value) { this.attributes[key] = value; - if (this.changeEvent) { - this.changeEvent(this); - } + this.changeEvents.forEach(function (event) { + event(); + }); }; SvgElementModel.prototype.get = function (key) { return this.attributes[key]; }; - SvgElementModel.prototype.onChange = function (callback) { - this.changeEvent = callback; + SvgElementModel.prototype.destroy = function () { + this.destroyEvents.forEach(function (event) { + event(); + }); + }; + SvgElementModel.prototype.on = function (event, callback) { + this[event + 'Events'].push(callback); }; SvgElementModel.decode = function () { return new this(); @@ -46,7 +52,12 @@ return ""; }; - var SvgPathModel = function (attributes) { + var SvgPathModel = function (userAttributes) { + var attributes = $.extend({ + color: "#FF0000", + points: [] + }, userAttributes); + SvgElementModel.call(this, attributes); }; SvgPathModel.prototype = Object.create(SvgElementModel.prototype); @@ -61,6 +72,9 @@ }; SvgPathModel.prototype.encode = function () { var pairedPoints = []; + var typeProperties = [ + this.get("color"), + ]; this.get("points").forEach(function (point) { pairedPoints.push( @@ -68,54 +82,64 @@ ); }); - return "P)(" + pairedPoints.join(")("); + return "P;" + typeProperties.join(";") + ")(" + pairedPoints.join(")("); }; SvgPathModel.decode = function (pathInfo) { - var points = []; - - $(pathInfo).each(function (i, point) { - points.push([point.x, point.y]); + pathInfo.points = pathInfo.points.map(function (point) { + return [point.x, point.y]; }); - return new SvgPathModel({points: points}); + return new SvgPathModel(pathInfo); }; - var TextModel = function (userAttributes) { + var SvgTextModel = function (userAttributes) { var attributes = $.extend({ text: "", x: 0, y: 0, - color: "red", + color: "#FF0000", fontSize: 20 }, userAttributes); SvgElementModel.call(this, attributes); }; - TextModel.prototype = Object.create(SvgElementModel.prototype); - TextModel.prototype.encode = function () { - return "T)(" + this.get("text") + ")(" + this.get("x") + ";" + this.get("y"); + SvgTextModel.prototype = Object.create(SvgElementModel.prototype); + SvgTextModel.prototype.encode = function () { + var typeProperties = [ + this.get("color"), + this.get("fontSize"), + ]; + + return "T;" + typeProperties.join(";") + ")(" + this.get("text") + ")(" + this.get("x") + ';' + this.get("y"); }; - TextModel.decode = function (textInfo) { - return new TextModel({ - text: textInfo.text, - x: textInfo.x, - y: textInfo.y - }); + SvgTextModel.decode = function (textInfo) { + return new SvgTextModel(textInfo); }; - var SvgPathView = function (model) { + var SvgElementView = function (model) { var self = this; this.model = model; - this.model.onChange(function () { + this.model.on('change', function () { self.render(); }); + this.model.on('destroy', function () { + self.el.remove(); + self.model = null; + }); + }; + + /** + * @param {SvgPathModel} model + * @constructor + */ + var SvgPathView = function (model) { + SvgElementView.call(this, model); this.el = document.createElementNS("http://www.w3.org/2000/svg", "path"); this.el.setAttribute("fill", "transparent"); - this.el.setAttribute("stroke", "red"); - this.el.setAttribute("stroke-width", "3"); }; + SvgPathView.prototype = Object.create(SvgElementView.prototype); SvgPathView.prototype.render = function () { var d = ""; @@ -128,39 +152,188 @@ ); this.el.setAttribute("d", d); + this.el.setAttribute('stroke', this.model.get('color')); + this.el.setAttribute("stroke-width", "3"); return this; }; - var TextView = function (model) { - var self = this; - - this.model = model; - this.model.onChange(function () { - self.render(); - }); + /** + * @param {SvgTextModel} model + * @constructor + */ + var SvgTextView = function (model) { + SvgElementView.call(this, model); this.el = document.createElementNS('http://www.w3.org/2000/svg', 'text'); - this.el.setAttribute('fill', this.model.get('color')); - this.el.setAttribute('font-size', this.model.get('fontSize')); this.el.setAttribute('stroke', 'none'); }; - TextView.prototype.render = function () { + SvgTextView.prototype = Object.create(SvgElementView.prototype); + SvgTextView.prototype.render = function () { this.el.setAttribute('x', this.model.get('x')); this.el.setAttribute('y', this.model.get('y')); + this.el.setAttribute('fill', this.model.get('color')); + this.el.setAttribute('font-size', this.model.get('fontSize')); this.el.textContent = this.model.get('text'); return this; }; + /** + * @param {SvgElementModel} model + * @constructor + */ + var ControllerView = function (model) { + var self = this; + + this.model = model; + this.model.on('change', function () { + self.render(); + }); + this.model.on('destroy', function () { + self.el.remove(); + self.model = null; + }); + + var elChoice = (function () { + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'choice[' + self.model.questionId + '][' + self.model.id + ']'; + + return input; + })(); + + var elHotspot = (function () { + var input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'hotspot[' + self.model.questionId + '][' + self.model.id + ']'; + + return input; + })(); + + var elText = (function () { + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'form-control'; + input.disabled = self.model instanceof SvgPathModel; + input.value = self.model instanceof SvgTextModel ? self.model.get('text') : '——————————'; + + return input; + })(); + elText.addEventListener('change', function () { + self.model.set('text', this.value); + }) + + var txtColor = (function () { + var input = document.createElement('input'); + input.type = 'color'; + input.value = self.model.get('color'); + input.style.border = '0 none'; + input.style.padding = '0'; + input.style.margin = '0'; + input.style.width = '26px'; + input.style.height = '26px'; + input.style.lineHeight = '28px'; + input.style.verticalAlign = 'middle'; + + return input; + })(); + txtColor.addEventListener('change', function () { + self.model.set('color', this.value); + }) + + var spanAddonColor = (function () { + var span = document.createElement('span'); + span.className = 'input-group-addon'; + span.style.padding = '0'; + + return span; + })(); + spanAddonColor.appendChild(txtColor); + + var txtSize = (function () { + var input = document.createElement('input'); + input.type = 'number'; + input.value = self.model.get('fontSize'); + input.step = '1'; + input.min = '15'; + input.max = '30'; + input.style.border = '0 none'; + input.style.padding = '0 0 0 4px'; + input.style.margin = '0'; + input.style.width = '41px'; + input.style.height = '26px'; + input.style.lineHeight = '28px'; + input.style.verticalAlign = 'middle'; + input.disabled = self.model instanceof SvgPathModel; + + return input; + })(); + txtSize.addEventListener('change', function () { + self.model.set('fontSize', this.value); + }) + + var spanAddonSize = (function () { + var span = document.createElement('span'); + span.className = 'input-group-addon'; + span.style.padding = '0'; + + return span; + })(); + spanAddonSize.appendChild(txtSize); + + var btnRemove = (function () { + var button = document.createElement('button'); + button.type = 'button'; + button.className = 'btn btn-default'; + button.innerHTML = ''; + + return button; + })(); + btnRemove.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + self.model.destroy(); + }); + + var spanGroupBtn = (function () { + var span = document.createElement('span'); + span.className = 'input-group-btn'; + + return span; + })(); + spanGroupBtn.appendChild(btnRemove); + + this.el = (function () { + var div = document.createElement('div'); + div.className = 'input-group input-group-sm'; + div.style.marginBottom = '10px'; + + return div; + })(); + this.el.appendChild(elText); + this.el.appendChild(elHotspot); + this.el.appendChild(elChoice); + this.el.appendChild(spanAddonColor); + this.el.appendChild(spanAddonSize); + this.el.appendChild(spanGroupBtn); + + this.render = function () { + elChoice.value = this.model.encode(); + elHotspot.value = this.model.encode(); + + return this; + } + }; + var ElementsCollection = function () { this.models = []; - this.length = 0; + this.lastId = 0; this.addEvent = null; - this.resetEvent = null; }; ElementsCollection.prototype.add = function (pathModel) { - pathModel.id = ++this.length; + pathModel.id = ++this.lastId; this.models.push(pathModel); @@ -172,18 +345,15 @@ return this.models[index]; }; ElementsCollection.prototype.reset = function () { - if (this.resetEvent) { - this.resetEvent(); - } + this.models.forEach(function (model) { + model.destroy(); + }) this.models = []; } ElementsCollection.prototype.onAdd = function (callback) { this.addEvent = callback; }; - ElementsCollection.prototype.onReset = function (callback) { - this.resetEvent = callback; - } var AnnotationCanvasView = function (elementsCollection, image, questionId) { var self = this; @@ -208,15 +378,23 @@ this.elementsCollection = elementsCollection; this.elementsCollection.onAdd(function (pathModel) { - self.renderElement(pathModel); - }); - this.elementsCollection.onReset(function () { - $(self.el.parentNode).children('input').remove(); + var svgElementView = null; + + if (pathModel instanceof SvgPathModel) { + svgElementView = new SvgPathView(pathModel); + } else if (pathModel instanceof SvgTextModel) { + svgElementView = new SvgTextView(pathModel); + } else { + return; + } - self.$el.children('text, path').remove(); + self.el.appendChild(svgElementView.render().el); - $('#annotation-toolbar-' + self.questionId + ' ul').empty(); - }) + var controllerView = new ControllerView(pathModel); + + $('#annotation-toolbar-' + self.questionId).append(controllerView.render().el); + $(controllerView.el).children('input').eq(0).focus(); + }); this.$rdbOptions = null; this.$btnReset = null; @@ -247,7 +425,8 @@ } var point = getPointOnImage(self.el, e.clientX, e.clientY); - elementModel = new TextModel({x: point.x, y: point.y, text: ''}); + elementModel = new SvgTextModel({x: point.x, y: point.y, text: ''}); + elementModel.questionId = self.questionId; self.elementsCollection.add(elementModel); elementModel = null; isMoving = false; @@ -261,6 +440,7 @@ } elementModel = new SvgPathModel({points: [[point.x, point.y]]}); + elementModel.questionId = self.questionId; self.elementsCollection.add(elementModel); isMoving = true; }) @@ -291,70 +471,6 @@ self.elementsCollection.reset(); }); }; - AnnotationCanvasView.prototype.renderElement = function (elementModel) { - var elementView = null, - self = this; - - if (elementModel instanceof SvgPathModel) { - elementView = new SvgPathView(elementModel); - } else if (elementModel instanceof TextModel) { - elementView = new TextView(elementModel); - } - - if (!elementView) { - return; - } - - $('') - .attr({ - type: 'hidden', - name: 'choice[' + this.questionId + '][' + elementModel.id + ']' - }) - .val(elementModel.encode()) - .appendTo(this.el.parentNode); - - $('') - .attr({ - type: 'hidden', - name: 'hotspot[' + this.questionId + '][' + elementModel.id + ']' - }) - .val(elementModel.encode()) - .appendTo(this.el.parentNode); - - this.el.appendChild(elementView.render().el); - - elementModel.onChange(function () { - elementView.render(); - - $('input[name="choice[' + self.questionId + '][' + elementModel.id + ']"]').val(elementModel.encode()); - $('input[name="hotspot[' + self.questionId + '][' + elementModel.id + ']"]').val(elementModel.encode()); - }); - - if (elementModel instanceof TextModel) { - $('') - .attr({ - type: 'text', - name: 'text[' + this.questionId + '][' + elementModel.id + ']' - }) - .addClass('form-control input-sm') - .on('change', function (e) { - elementModel.set('text', this.value); - - e.preventDefault(); - }) - .on('keypress', function (e) { - if (13 === e.keyCode) { - e.preventDefault(); - - elementModel.set('text', this.value); - } - }) - .val(elementModel.get('text')) - .appendTo('#annotation-toolbar-' + this.questionId + ' ul') - .wrap('
  • ') - .focus(); - } - }; window.AnnotationQuestion = function (userSettings) { $(function () { @@ -386,12 +502,14 @@ /** @namespace questionInfo.answers.paths */ $.each(questionInfo.answers.paths, function (i, pathInfo) { var pathModel = SvgPathModel.decode(pathInfo); + pathModel.questionId = settings.questionId; elementsCollection.add(pathModel); }); /** @namespace questionInfo.answers.texts */ $(questionInfo.answers.texts).each(function (i, textInfo) { - var textModel = TextModel.decode(textInfo); + var textModel = SvgTextModel.decode(textInfo); + textModel.questionId = settings.questionId; elementsCollection.add(textModel); }); };