Exercise: Allow change size/color in annotation - refs BT#18831

pull/4041/head
Angel Fernando Quiroz Campos 4 years ago
parent 992eddb87c
commit be1ac30c08
  1. 13
      main/exercise/annotation_user.php
  2. 51
      main/inc/lib/exercise.lib.php
  3. 360
      main/inc/lib/javascript/annotation/js/annotation.js

@ -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;
}

@ -1704,34 +1704,31 @@ HOTSPOT;
<div id="annotation-canvas-'.$questionId.'" class="annotation-canvas center-block">
</div>
</div>
<div class="col-sm-4 col-md-3">
<div class="well well-sm" id="annotation-toolbar-'.$questionId.'">
<div class="btn-toolbar">
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-default active"
aria-label="'.get_lang('AddAnnotationPath').'">
<input
type="radio" value="0"
name="'.$questionId.'-options" autocomplete="off" checked>
<span class="fa fa-pencil" aria-hidden="true"></span>
</label>
<label class="btn btn-default"
aria-label="'.get_lang('AddAnnotationText').'">
<input
type="radio" value="1"
name="'.$questionId.'-options" autocomplete="off">
<span class="fa fa-font fa-fw" aria-hidden="true"></span>
</label>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default btn-small"
title="'.get_lang('ClearAnswers').'"
id="btn-reset-'.$questionId.'">
<span class="fa fa-times-rectangle fa-fw" aria-hidden="true"></span>
</button>
</div>
<div class="col-sm-4 col-md-3" id="annotation-toolbar-'.$questionId.'">
<div class="btn-toolbar" style="margin-top: 0;">
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-default active"
aria-label="'.get_lang('AddAnnotationPath').'">
<input
type="radio" value="0"
name="'.$questionId.'-options" autocomplete="off" checked>
<span class="fa fa-pencil" aria-hidden="true"></span>
</label>
<label class="btn btn-default"
aria-label="'.get_lang('AddAnnotationText').'">
<input
type="radio" value="1"
name="'.$questionId.'-options" autocomplete="off">
<span class="fa fa-font fa-fw" aria-hidden="true"></span>
</label>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default btn-small"
title="'.get_lang('ClearAnswers').'"
id="btn-reset-'.$questionId.'">
<span class="fa fa-times-rectangle fa-fw" aria-hidden="true"></span>
</button>
</div>
<ul class="list-unstyled"></ul>
</div>
</div>
</div>

@ -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 = '<span class="fa fa-trash text-danger" aria-hidden="true"></span>';
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;
}
$('<input>')
.attr({
type: 'hidden',
name: 'choice[' + this.questionId + '][' + elementModel.id + ']'
})
.val(elementModel.encode())
.appendTo(this.el.parentNode);
$('<input>')
.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) {
$('<input>')
.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('<li class="form-group"></li>')
.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);
});
};

Loading…
Cancel
Save