/* For licensing terms, see /license.txt */ (function (window, $) { "use strict"; function getPointOnImage(referenceElement, x, y) { var pointerPosition = { left: x + window.scrollX, top: y + window.scrollY }, canvasOffset = { x: referenceElement.getBoundingClientRect().left + window.scrollX, y: referenceElement.getBoundingClientRect().top + window.scrollY }; return { x: Math.round(pointerPosition.left - canvasOffset.x), y: Math.round(pointerPosition.top - canvasOffset.y) }; } /** * @param {Object} attributes * @constructor */ var SvgElementModel = function (attributes) { this.attributes = attributes; this.id = 0; this.questionId = 0; this.changeEvents = []; this.destroyEvents = []; }; /** * @param {string} key * @param {*} value */ SvgElementModel.prototype.set = function (key, value) { this.attributes[key] = value; this.changeEvents.forEach(function (event) { event(); }); }; SvgElementModel.prototype.get = function (key) { return this.attributes[key]; }; SvgElementModel.prototype.destroy = function () { this.destroyEvents.forEach(function (event) { event(); }); }; /** * @param {string} eventName * @param {(SvgElementModel~changeEvents|SvgElementModel~destroyEvents)} callback */ SvgElementModel.prototype.on = function (eventName, callback) { this[eventName + 'Events'].push(callback); }; /** * @abstract * @static * @param {Object} info * @returns {SvgElementModel} */ SvgElementModel.decode = function (info) { return new this(); }; /** * @abstract * @returns {string} */ SvgElementModel.prototype.encode = function () { return ""; }; /** * @param {Object} userAttributes * @constructor * @extends SvgElementModel */ var SvgPathModel = function (userAttributes) { var attributes = $.extend({ color: "#FF0000", points: [] }, userAttributes); SvgElementModel.call(this, attributes); }; SvgPathModel.prototype = Object.create(SvgElementModel.prototype); SvgPathModel.prototype.addPoint = function (x, y) { x = parseInt(x); y = parseInt(y); var points = this.get("points"); points.push([x, y]); this.set("points", points); }; SvgPathModel.prototype.encode = function () { var pairedPoints = []; var typeProperties = [ this.get("color"), ]; this.get("points").forEach(function (point) { pairedPoints.push( point.join(";") ); }); return "P;" + typeProperties.join(";") + ")(" + pairedPoints.join(")("); }; /** * @static * @param {Object} pathInfo * @returns {SvgPathModel} */ SvgPathModel.decode = function (pathInfo) { pathInfo.points = pathInfo.points.map(function (point) { return [point.x, point.y]; }); return new SvgPathModel(pathInfo); }; /** * @param {Object} userAttributes * @constructor * @extends SvgElementModel */ var SvgTextModel = function (userAttributes) { var attributes = $.extend({ text: "", x: 0, y: 0, color: "#FF0000", fontSize: 20 }, userAttributes); SvgElementModel.call(this, attributes); }; 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"); }; /** * @static * @param {Object} textInfo * @returns {SvgTextModel} */ SvgTextModel.decode = function (textInfo) { return new SvgTextModel(textInfo); }; /** * @param {SvgElementModel} model * @constructor */ var SvgElementView = function (model) { var self = this; this.model = model; this.model.on('change', function () { self.render(); }); this.model.on('destroy', function () { self.el.remove(); }); }; /** * @param {SvgPathModel} model * @constructor * @extends SvgElementView */ var SvgPathView = function (model) { SvgElementView.call(this, model); this.el = document.createElementNS("http://www.w3.org/2000/svg", "path"); this.el.setAttribute("fill", "transparent"); }; SvgPathView.prototype = Object.create(SvgElementView.prototype); SvgPathView.prototype.render = function () { var d = ""; $.each( this.model.get("points"), function (i, point) { d += (i === 0) ? "M" : " L "; d += point[0] + " " + point[1]; } ); this.el.setAttribute("d", d); this.el.setAttribute('stroke', this.model.get('color')); this.el.setAttribute("stroke-width", "3"); return this; }; /** * @param {SvgTextModel} model * @constructor * @extends SvgElementView */ var SvgTextView = function (model) { SvgElementView.call(this, model); this.el = document.createElementNS('http://www.w3.org/2000/svg', 'text'); this.el.setAttribute('stroke', 'none'); }; 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(); }); 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; return input; })(); elText.addEventListener('change', function () { commandsHistory.add(new TextElementCommand(self.model, this.value)); self.model.set('text', this.value); }) var txtColor = (function () { var input = document.createElement('input'); input.type = '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 () { commandsHistory.add(new ColorElementCommand(self.model, this.value)); 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.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 () { commandsHistory.add(new SizeElementCommand(self.model, this.value)); 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(); elText.value = self.model instanceof SvgTextModel ? self.model.get('text') : '——————————'; txtColor.value = this.model.get('color'); txtSize.value = this.model.get('fontSize'); return this; } }; /** * @constructor */ var ElementsCollection = function () { /** * @type {SvgElementModel[]} */ this.models = []; this.addEvent = null; var lastId = 0; /** * @param {SvgElementModel} pathModel */ this.add = function (pathModel) { pathModel.id = ++lastId; this.models.push(pathModel); if (this.addEvent) { this.addEvent(pathModel); } }; /** * @param {number} index * @returns {SvgElementModel} */ this.get = function (index) { return this.models[index]; }; this.reset = function () { this.models.forEach(function (model) { model.destroy(); }) this.models = []; }; /** * @param {ElementsCollection~addEvent} callback */ this.onAdd = function (callback) { this.addEvent = callback; }; }; /** * @param {ElementsCollection} elementsCollection * @param {Image} image * @param {number} questionId * @constructor */ var AnnotationCanvasView = function (elementsCollection, image, questionId) { var self = this; this.questionId = questionId; this.image = image; var svgImage = (function () { var image = document.createElementNS('http://www.w3.org/2000/svg', 'image'); image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', self.image.src); image.setAttribute('width', self.image.width); image.setAttribute('height', self.image.height); return image; })(); this.el = (function () { var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('version', '1.1'); svg.setAttribute('viewBox', '0 0 ' + self.image.width + ' ' + self.image.height); svg.setAttribute('width', self.image.width); svg.setAttribute('height', self.image.height); return svg; })(); this.el.appendChild(svgImage); this.elementsCollection = elementsCollection; this.elementsCollection.onAdd(function (pathModel) { var svgElementView = null; if (pathModel instanceof SvgPathModel) { svgElementView = new SvgPathView(pathModel); } else if (pathModel instanceof SvgTextModel) { svgElementView = new SvgTextView(pathModel); } else { return; } self.el.appendChild(svgElementView.render().el); var controllerView = new ControllerView(pathModel); $('#annotation-toolbar-' + self.questionId).append(controllerView.render().el); $(controllerView.el).children('input').eq(0).focus(); }); var $rdbOptions = null; var $btnReset = null; var $btnUndo = null; var $btnRedo = null; this.render = function () { $rdbOptions = $('[name="' + this.questionId + '-options"]'); $btnReset = $('#btn-reset-' + this.questionId); $btnUndo = $('#btn-undo-' + this.questionId); $btnRedo = $('#btn-redo-' + this.questionId); setEvents(); return this; }; function setEvents() { var isMoving = false, elementModel = null; $(self.el) .on('dragstart', function (e) { e.preventDefault(); }) .on('click', function (e) { e.preventDefault(); if ("1" !== $rdbOptions.filter(':checked').val()) { return; } var point = getPointOnImage(self.el, e.clientX, e.clientY); elementModel = new SvgTextModel({x: point.x, y: point.y, text: ''}); elementModel.questionId = self.questionId; commandsHistory.add(new AddElementCommand(self.elementsCollection, elementModel)); self.elementsCollection.add(elementModel); elementModel = null; isMoving = false; }) .on('mousedown', function (e) { e.preventDefault(); var point = getPointOnImage(self.el, e.clientX, e.clientY); if (isMoving || "0" !== $rdbOptions.filter(':checked').val() || elementModel) { return; } elementModel = new SvgPathModel({points: [[point.x, point.y]]}); elementModel.questionId = self.questionId; self.elementsCollection.add(elementModel); isMoving = true; }) .on('mousemove', function (e) { e.preventDefault(); if (!isMoving || "0" !== $rdbOptions.filter(':checked').val() || !elementModel) { return; } var point = getPointOnImage(self.el, e.clientX, e.clientY); elementModel.addPoint(point.x, point.y); }) .on('mouseup', function (e) { e.preventDefault(); if (!isMoving || "0" !== $rdbOptions.filter(':checked').val() || !elementModel) { return; } commandsHistory.add(new AddElementCommand(self.elementsCollection, elementModel)); elementModel = null; isMoving = false; }); $btnReset.on('click', function (e) { e.preventDefault(); commandsHistory.add(new ResetCommand(self.elementsCollection)); self.elementsCollection.reset(); }); $btnUndo.on('click', function () { commandsHistory.undo(); }); $btnRedo.on('click', function () { commandsHistory.redo(); }); } }; /** * @constructor * @abstract */ function Command() {} /** * @abstract */ Command.prototype.before = function () { throw new Error('Implement'); } /** * @abstract */ Command.prototype.after = function () { throw new Error('Implement'); } /** * @param {ElementsCollection} collection * @param {SvgElementModel} model * @constructor */ function AddElementCommand(collection, model) { Command.call(this); this.collection = collection; this.model = model; } AddElementCommand.prototype = Object.create(Command.prototype); AddElementCommand.prototype.after = function () { this.collection.add(this.model); }; AddElementCommand.prototype.before = function () { this.model.destroy(); }; /** * @param {SvgElementModel} model * @param {string} attribute * @param {*} newValue * @constructor * @abstract * @extends Command */ function ElementCommand(model, attribute, newValue) { Command.call(this); this.model = model; this.attribute = attribute; this.oldValue = this.model.get(this.attribute); this.newValue = newValue; } ElementCommand.prototype = Object.create(Command.prototype); ElementCommand.prototype.after = function () { this.model.set(this.attribute, this.newValue); }; ElementCommand.prototype.before = function () { this.model.set(this.attribute, this.oldValue); }; /** * @param {SvgElementModel} model * @param {*} newValue * @constructor * @extends ElementCommand */ function TextElementCommand(model, newValue) { ElementCommand.call(this, model, 'text', newValue); } TextElementCommand.prototype = Object.create(ElementCommand.prototype); /** * @param {SvgElementModel} model * @param {*} newValue * @constructor * @extends ElementCommand */ function ColorElementCommand(model, newValue) { ElementCommand.call(this, model, 'color', newValue); } ColorElementCommand.prototype = Object.create(ElementCommand.prototype); /** * @param {SvgElementModel} model * @param {*} newValue * @constructor * @extends ElementCommand */ function SizeElementCommand(model, newValue) { ElementCommand.call(this, model, 'fontSize', newValue); } SizeElementCommand.prototype = Object.create(ElementCommand.prototype); /** * * @param {ElementsCollection} collection * @constructor * @extends Command */ function ResetCommand(collection) { Command.call(this); this.collection = collection; this.oldModels = collection.models; } ResetCommand.prototype = Object.create(Command.prototype); ResetCommand.prototype.after = function () { this.collection.reset(); }; ResetCommand.prototype.before = function () { var self = this; this.oldModels.forEach(function (model) { self.collection.add(model); }); }; function CommandHistory() { var index = -1; /** * @type {Command[]} */ var commands = []; /** * @param {Command} command */ this.add = function (command) { if (index > -1) { commands = commands.slice(0, index + 1); } else { commands = []; } commands.push(command); ++index; } this.undo = function () { (commands, index); if (-1 === index) { return; } var command = commands[index]; command.before(); --index; }; this.redo = function () { if (index + 1 === commands.length) { return; } ++index; var command = commands[index]; command.after(); }; }; var commandsHistory = new CommandHistory(); window.AnnotationQuestion = function (userSettings) { $(function () { var settings = $.extend( { questionId: 0, exerciseId: 0, relPath: '/' }, userSettings ), xhrUrl = 'exercise/annotation_user.php?' + _p.web_cid_query, $container = $('#annotation-canvas-' + settings.questionId); $ .getJSON(settings.relPath + xhrUrl, { question_id: parseInt(settings.questionId), exe_id: parseInt(settings.exerciseId), course_id: parseInt(settings.courseId) }) .done(function (questionInfo) { var image = new Image(); image.onload = function () { var elementsCollection = new ElementsCollection(), canvas = new AnnotationCanvasView(elementsCollection, this, parseInt(settings.questionId)); $container.html(canvas.render().el); /** @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 = SvgTextModel.decode(textInfo); textModel.questionId = settings.questionId; elementsCollection.add(textModel); }); }; image.src = questionInfo.image.path; }); }); }; })(window, window.jQuery);